+ * For compound modifiers such as {@code public abstract} use bitwise OR ({@code |}) of multiple Modifiers, {@code
+ * Modifier.PUBLIC | Modifier.ABSTRACT}.
+ *
+ * Pass {@code 0} for {@code modifiers} to indicate that the method has no modifiers.
+ *
+ * @param modifiers The {@link Modifier Modifiers} for the method.
+ * @return The updated MethodCustomization object.
+ * @throws IllegalArgumentException If the {@code modifier} is less than to {@code 0} or any {@link Modifier}
+ * included in the bitwise OR isn't a valid method {@link Modifier}.
+ */
+ public MethodCustomization setModifier(int modifiers) {
+ Utils.replaceModifier(symbol, editor, languageClient, "(?:.+ )?(\\w+ )" + methodName + "\\(",
+ "$1" + methodName + "(", Modifier.methodModifiers(), modifiers);
+
+ return refreshCustomization(methodSignature);
+ }
+
+ /**
+ * Replace the parameters of the method.
+ *
+ * @param newParameters New method parameters.
+ * @return The updated MethodCustomization object.
+ */
+ public MethodCustomization replaceParameters(String newParameters) {
+ return replaceParameters(newParameters, null);
+ }
+
+ /**
+ * Replaces the parameters of the method and adds any additional imports required by the new parameters.
+ *
+ * @param newParameters New method parameters.
+ * @param importsToAdd Any additional imports required by the method. These will be custom types or types that
+ * are ambiguous on which to use such as {@code List} or the utility class {@code Arrays}.
+ * @return A new MethodCustomization representing the updated method.
+ */
+ public MethodCustomization replaceParameters(String newParameters, List importsToAdd) {
+ String newSignature = methodName + "(" + newParameters + ")";
+
+ ClassCustomization classCustomization = new PackageCustomization(editor, languageClient, packageName)
+ .getClass(className);
+
+ ClassCustomization updatedClassCustomization = Utils.addImports(importsToAdd, classCustomization,
+ classCustomization::refreshSymbol);
+
+ return Utils.replaceParameters(newParameters, updatedClassCustomization.getMethod(methodSignature),
+ () -> updatedClassCustomization.getMethod(newSignature));
+ }
+
+ /**
+ * Replace the body of the method.
+ *
+ * @param newBody New method body.
+ * @return The updated MethodCustomization object.
+ */
+ public MethodCustomization replaceBody(String newBody) {
+ return replaceBody(newBody, null);
+ }
+
+ /**
+ * Replaces the body of the method and adds any additional imports required by the new body.
+ *
+ * @param newBody New method body.
+ * @param importsToAdd Any additional imports required by the method. These will be custom types or types that
+ * are ambiguous on which to use such as {@code List} or the utility class {@code Arrays}.
+ * @return A new MethodCustomization representing the updated method.
+ */
+ public MethodCustomization replaceBody(String newBody, List importsToAdd) {
+ ClassCustomization classCustomization = new PackageCustomization(editor, languageClient, packageName)
+ .getClass(className);
+
+ ClassCustomization updatedClassCustomization = Utils.addImports(importsToAdd, classCustomization,
+ classCustomization::refreshSymbol);
+
+ return Utils.replaceBody(newBody, updatedClassCustomization.getMethod(methodSignature),
+ () -> updatedClassCustomization.getMethod(methodSignature));
+ }
+
+ /**
+ * Change the return type of the method. The new return type will be automatically imported.
+ *
+ *
+ * The {@code returnValueFormatter} can be used to transform the return value. If the original return type is {@code
+ * void}, simply pass the new return expression to {@code returnValueFormatter}; if the new return type is {@code
+ * void}, pass {@code null} to {@code returnValueFormatter}; if either the original return type nor the new return
+ * type is {@code void}, the {@code returnValueFormatter} should be a String formatter that contains exactly 1
+ * instance of {@code %s}.
+ *
+ * @param newReturnType the simple name of the new return type
+ * @param returnValueFormatter the return value String formatter as described above
+ * @return the current method customization for chaining
+ */
+ public MethodCustomization setReturnType(String newReturnType, String returnValueFormatter) {
+ return setReturnType(newReturnType, returnValueFormatter, false);
+ }
+
+ /**
+ * Change the return type of the method. The new return type will be automatically imported.
+ *
+ *
+ * The {@code returnValueFormatter} can be used to transform the return value. If the original return type is {@code
+ * void}, simply pass the new return expression to {@code returnValueFormatter}; if the new return type is {@code
+ * void}, pass {@code null} to {@code returnValueFormatter}; if either the original return type nor the new return
+ * type is {@code void}, the {@code returnValueFormatter} should be a String formatter that contains exactly 1
+ * instance of {@code %s}.
+ *
+ * @param newReturnType the simple name of the new return type
+ * @param returnValueFormatter the return value String formatter as described above
+ * @param replaceReturnStatement if set to {@code true}, the return statement will be replaced by the provided
+ * returnValueFormatter text with exactly one instance of {@code %s}. If set to true, appropriate semi-colons,
+ * parentheses, opening and closing of code blocks have to be taken care of in the {@code returnValueFormatter}.
+ * @return the current method customization for chaining
+ */
+ public MethodCustomization setReturnType(String newReturnType, String returnValueFormatter,
+ boolean replaceReturnStatement) {
+ List edits = new ArrayList<>();
+
+ int line = symbol.getLocation().getRange().getStart().getLine();
+ Position start = new Position(line, 0);
+ String oldLineContent = editor.getFileLine(fileName, line);
+ Position end = new Position(line, oldLineContent.length());
+ String newLineContent = oldLineContent.replaceFirst("(\\w.* )?(\\w+) " + methodName + "\\(",
+ "$1" + newReturnType + " " + methodName + "(");
+ TextEdit signatureEdit = new TextEdit();
+ signatureEdit.setNewText(newLineContent);
+ signatureEdit.setRange(new Range(start, end));
+ edits.add(signatureEdit);
+
+ String methodIndent = Utils.getIndent(editor.getFileLine(fileName, line));
+ String methodContentIndent = Utils.getIndent(editor.getFileLine(fileName, line + 1));
+ String oldReturnType = oldLineContent.replaceAll(" " + methodName + "\\(.*", "")
+ .replaceFirst(methodIndent + "(\\w.* )?", "").trim();
+ int returnLine = -1;
+ while (!oldLineContent.startsWith(methodIndent + "}")) {
+ if (oldLineContent.contains("return ")) {
+ returnLine = line;
+ }
+ oldLineContent = editor.getFileLine(fileName, ++line);
+ }
+ if (returnLine == -1) {
+ // no return statement, originally void return type
+ editor.insertBlankLine(fileName, line, false);
+ FileEvent blankLineEvent = new FileEvent();
+ blankLineEvent.setUri(fileUri);
+ blankLineEvent.setType(FileChangeType.Changed);
+ languageClient.notifyWatchedFilesChanged(Collections.singletonList(blankLineEvent));
+
+ TextEdit returnEdit = new TextEdit();
+ returnEdit.setRange(new Range(new Position(line, 0), new Position(line, 0)));
+ returnEdit.setNewText(methodContentIndent + "return " + returnValueFormatter + ";");
+ edits.add(returnEdit);
+ } else if (newReturnType.equals("void")) {
+ // remove return statement
+ TextEdit returnEdit = new TextEdit();
+ returnEdit.setNewText("");
+ returnEdit.setRange(new Range(new Position(returnLine, 0), new Position(line, 0)));
+ edits.add(returnEdit);
+ } else {
+ // replace return statement
+ TextEdit returnValueEdit = new TextEdit();
+ String returnLineText = editor.getFileLine(fileName, returnLine);
+ returnValueEdit.setRange(new Range(new Position(returnLine, 0), new Position(returnLine, returnLineText.length())));
+ returnValueEdit.setNewText(returnLineText.replace("return ", oldReturnType + " returnValue = "));
+ edits.add(returnValueEdit);
+
+ editor.insertBlankLine(fileName, line, false);
+ FileEvent blankLineEvent = new FileEvent();
+ blankLineEvent.setUri(fileUri);
+ blankLineEvent.setType(FileChangeType.Changed);
+ languageClient.notifyWatchedFilesChanged(Collections.singletonList(blankLineEvent));
+
+ TextEdit returnEdit = new TextEdit();
+ returnEdit.setRange(new Range(new Position(line, 0), new Position(line, 0)));
+
+ if (replaceReturnStatement) {
+ returnEdit.setNewText(String.format(returnValueFormatter, "returnValue"));
+ } else {
+ returnEdit.setNewText(methodContentIndent + "return " + String.format(returnValueFormatter, "returnValue") + ";");
+ }
+
+ edits.add(returnEdit);
+ }
+
+ WorkspaceEdit workspaceEdit = new WorkspaceEdit();
+ workspaceEdit.setChanges(Collections.singletonMap(fileUri, edits));
+ Utils.applyWorkspaceEdit(workspaceEdit, editor, languageClient);
+
+ Utils.organizeImportsOnRange(languageClient, editor, fileUri, new Range(start, end));
+
+ String newMethodSignature = methodSignature.replace(oldReturnType + " " + methodName, newReturnType + " " + methodName);
+
+ return refreshCustomization(newMethodSignature);
+ }
+
+ private MethodCustomization refreshCustomization(String methodSignature) {
+ return new PackageCustomization(editor, languageClient, packageName)
+ .getClass(className)
+ .getMethod(methodSignature);
+ }
+}
diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/PackageCustomization.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/PackageCustomization.java
new file mode 100644
index 0000000000..2f086943be
--- /dev/null
+++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/PackageCustomization.java
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.typespec.http.client.generator.core.customization;
+
+import com.microsoft.typespec.http.client.generator.core.customization.implementation.Utils;
+import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.EclipseLanguageClient;
+import org.eclipse.lsp4j.SymbolInformation;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * The package level customization for an AutoRest generated client library.
+ */
+public final class PackageCustomization {
+ private final EclipseLanguageClient languageClient;
+ private final Editor editor;
+ private final String packageName;
+
+ PackageCustomization(Editor editor, EclipseLanguageClient languageClient, String packageName) {
+ this.editor = editor;
+ this.languageClient = languageClient;
+ this.packageName = packageName;
+ }
+
+ /**
+ * Gets the class level customization for a Java class in the package.
+ *
+ * @param className the simple name of the class
+ * @return the class level customization
+ */
+ public ClassCustomization getClass(String className) {
+ String packagePath = packageName.replace(".", "/");
+ Optional classSymbol = languageClient.findWorkspaceSymbol(className).stream()
+ // findWorkspace symbol finds all classes that contain the classname term
+ // The filter that checks the filename only works if there are no nested classes
+ // So, when customizing client classes that contain service interface, this can incorrectly return
+ // the service interface instead of the client class. So, we should add another check for exact name match
+ .filter(si -> si.getName().equals(className))
+ .filter(si -> si.getLocation().getUri().endsWith(packagePath + "/" + className + ".java"))
+ .findFirst();
+
+ return Utils.returnIfPresentOrThrow(classSymbol,
+ symbol -> new ClassCustomization(editor, languageClient, packageName, className, symbol),
+ () -> new IllegalArgumentException(className + " does not exist in package " + packageName));
+ }
+
+ /**
+ * This method lists all the classes in this package.
+ * @return A list of classes that are in this package.
+ */
+ public List listClasses() {
+ return languageClient.findWorkspaceSymbol("*")
+ .stream()
+ .filter(si -> si.getContainerName().equals(packageName))
+ .map(classSymbol -> new ClassCustomization(editor, languageClient, packageName,
+ classSymbol.getName(), classSymbol))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/PropertyCustomization.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/PropertyCustomization.java
new file mode 100644
index 0000000000..ddadccef8c
--- /dev/null
+++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/PropertyCustomization.java
@@ -0,0 +1,164 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.typespec.http.client.generator.core.customization;
+
+import com.microsoft.typespec.http.client.generator.core.customization.implementation.Utils;
+import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.EclipseLanguageClient;
+import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.models.JavaCodeActionKind;
+import org.eclipse.lsp4j.CodeAction;
+import org.eclipse.lsp4j.SymbolInformation;
+import org.eclipse.lsp4j.SymbolKind;
+import org.eclipse.lsp4j.WorkspaceEdit;
+
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+
+/**
+ * Customization for an AutoRest generated instance property.
+ *
+ * For constant property customizations use {@link ConstantCustomization}.
+ */
+public final class PropertyCustomization extends CodeCustomization {
+ private static final Pattern METHOD_PARAMS_CAPTURE = Pattern.compile("\\(.*\\)");
+
+ private final String packageName;
+ private final String className;
+ private final String propertyName;
+
+ PropertyCustomization(Editor editor, EclipseLanguageClient languageClient, String packageName, String className,
+ SymbolInformation symbol, String propertyName) {
+ super(editor, languageClient, symbol);
+ this.packageName = packageName;
+ this.className = className;
+ this.propertyName = propertyName;
+ }
+
+ /**
+ * Gets the name of the class that contains this property.
+ *
+ * @return The name of the class that contains this property.
+ */
+ public String getClassName() {
+ return className;
+ }
+
+ /**
+ * Gets the name of this property.
+ *
+ * @return The name of this property.
+ */
+ public String getPropertyName() {
+ return propertyName;
+ }
+
+ /**
+ * Rename a property in the class. This is a refactor operation. All references of the property will be renamed and
+ * the getter and setter method(s) for this property will be renamed accordingly as well.
+ *
+ * @param newName the new name for the property
+ * @return the current class customization for chaining
+ */
+ public PropertyCustomization rename(String newName) {
+ List symbols = languageClient.listDocumentSymbols(fileUri)
+ .stream().filter(si -> si.getName().toLowerCase().contains(propertyName.toLowerCase()))
+ .collect(Collectors.toList());
+ String propertyPascalName = propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
+ String newPascalName = newName.substring(0, 1).toUpperCase() + newName.substring(1);
+
+ List edits = new ArrayList<>();
+ for (SymbolInformation symbol : symbols) {
+ if (symbol.getKind() == SymbolKind.Field) {
+ edits.add(languageClient.renameSymbol(fileUri, symbol.getLocation().getRange().getStart(), newName));
+ } else if (symbol.getKind() == SymbolKind.Method) {
+ String methodName = symbol.getName().replace(propertyPascalName, newPascalName)
+ .replace(propertyName, newName);
+ methodName = METHOD_PARAMS_CAPTURE.matcher(methodName).replaceFirst("");
+ edits.add(languageClient.renameSymbol(fileUri, symbol.getLocation().getRange().getStart(), methodName));
+ }
+ }
+
+ Utils.applyWorkspaceEdits(edits, editor, languageClient);
+ return refreshCustomization(newName);
+ }
+
+ /**
+ * Add an annotation to a property in the class.
+ *
+ * @param annotation the annotation to add. The leading @ can be omitted.
+ * @return the current property customization for chaining
+ */
+ public PropertyCustomization addAnnotation(String annotation) {
+ return Utils.addAnnotation(annotation, this, () -> refreshCustomization(propertyName));
+ }
+
+ /**
+ * Remove an annotation from the property.
+ *
+ * @param annotation the annotation to remove from the property. The leading @ can be omitted.
+ * @return the current property customization for chaining
+ */
+ public PropertyCustomization removeAnnotation(String annotation) {
+ return Utils.removeAnnotation(this, compilationUnit -> compilationUnit.getClassByName(className).get()
+ .getFieldByName(propertyName).get()
+ .getAnnotationByName(Utils.cleanAnnotationName(annotation)), () -> refreshCustomization(propertyName));
+ }
+
+ /**
+ * Generates a getter and a setter method(s) for a property in the class. This is a refactor operation. If a getter
+ * or a setter is already available on the class, the current getter or setter will be kept.
+ *
+ * @return the current class customization for chaining
+ */
+ public PropertyCustomization generateGetterAndSetter() {
+ Optional generateAccessors = languageClient.listCodeActions(fileUri, symbol.getLocation().getRange(),
+ JavaCodeActionKind.SOURCE_GENERATE_ACCESSORS.toString())
+ .stream().filter(ca -> ca.getKind().equals(JavaCodeActionKind.SOURCE_GENERATE_ACCESSORS.toString()))
+ .findFirst();
+ if (generateAccessors.isPresent()) {
+ Utils.applyWorkspaceEdit(generateAccessors.get().getEdit(), editor, languageClient);
+
+ String setterMethod = "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
+ new PackageCustomization(editor, languageClient, packageName)
+ .getClass(className)
+ .getMethod(setterMethod).setReturnType(className, "this");
+ }
+
+ return this;
+ }
+
+ /**
+ * Replace the modifier for this property.
+ *
+ * For compound modifiers such as {@code public final} use bitwise OR ({@code |}) of multiple Modifiers, {@code
+ * Modifier.PUBLIC | Modifier.FINAL}.
+ *
+ * Pass {@code 0} for {@code modifiers} to indicate that the property has no modifiers.
+ *
+ * @param modifiers The {@link Modifier Modifiers} for the property.
+ * @return The updated PropertyCustomization object.
+ * @throws IllegalArgumentException If the {@code modifier} is less than {@code 0} or any {@link Modifier} included
+ * in the bitwise OR isn't a valid property {@link Modifier}.
+ */
+ public PropertyCustomization setModifier(int modifiers) {
+ String target = " *(?:(?:public|protected|private|static|final|transient|volatile) ?)*(.* )";
+ languageClient.listDocumentSymbols(symbol.getLocation().getUri())
+ .stream().filter(si -> si.getKind() == SymbolKind.Field && si.getName().equals(propertyName))
+ .findFirst()
+ .ifPresent(symbolInformation -> Utils.replaceModifier(symbolInformation, editor, languageClient,
+ target + propertyName, "$1" + propertyName, Modifier.fieldModifiers(), modifiers));
+
+ return refreshCustomization(propertyName);
+ }
+
+ private PropertyCustomization refreshCustomization(String propertyName) {
+ return new PackageCustomization(editor, languageClient, packageName)
+ .getClass(className)
+ .getProperty(propertyName);
+ }
+}
diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/Utils.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/Utils.java
new file mode 100644
index 0000000000..cde89f581b
--- /dev/null
+++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/Utils.java
@@ -0,0 +1,521 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.typespec.http.client.generator.core.customization.implementation;
+
+import com.microsoft.typespec.http.client.generator.core.customization.ClassCustomization;
+import com.microsoft.typespec.http.client.generator.core.customization.CodeCustomization;
+import com.microsoft.typespec.http.client.generator.core.customization.Editor;
+import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.EclipseLanguageClient;
+import com.github.javaparser.StaticJavaParser;
+import com.github.javaparser.ast.CompilationUnit;
+import com.github.javaparser.ast.expr.AnnotationExpr;
+import org.eclipse.lsp4j.CodeActionKind;
+import org.eclipse.lsp4j.FileChangeType;
+import org.eclipse.lsp4j.FileEvent;
+import org.eclipse.lsp4j.Position;
+import org.eclipse.lsp4j.Range;
+import org.eclipse.lsp4j.SymbolInformation;
+import org.eclipse.lsp4j.TextEdit;
+import org.eclipse.lsp4j.WorkspaceEdit;
+
+import java.io.File;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class Utils {
+ /**
+ * This pattern determines the indentation of the passed string. Effectively it creates a group containing all
+ * spaces before the first word character.
+ */
+ public static final Pattern INDENT_DETERMINATION_PATTERN = Pattern.compile("^(\\s*).*$");
+
+ /**
+ * This pattern matches a Java package declaration.
+ */
+ private static final Pattern PACKAGE_PATTERN = Pattern.compile("package\\s[\\w\\.]+;");
+
+ /**
+ * This pattern matches anything then the space.
+ */
+ public static final Pattern ANYTHING_THEN_SPACE_PATTERN = Pattern.compile(".* ");
+
+ /*
+ * This pattern determines if a line is a beginning of constructor or method. The following is an explanation of
+ * the pattern:
+ *
+ * 1. Capture all leading space characters.
+ * 2. Capture all modifiers for the constructor or method.
+ * 3. If a method, capture the return type.
+ * 4. Capture the name of the constructor or method.
+ *
+ * The following are the groups return:
+ *
+ * 1. The entire matching declaration from the beginning of the string used to determine the beginning offset of the
+ * parameters in the constructor or method.
+ * 2. Any modifiers for the constructor or method. This may be empty/null.
+ * 3. If a method, the return type. If a constructor, empty/null.
+ * 4. The name of the constructor or method.
+ */
+ private static final Pattern BEGINNING_OF_PARAMETERS_PATTERN =
+ Pattern.compile("^(\\s*(?:([\\w\\s]*?)\\s)?(?:([a-zA-Z$_][\\w]*?)\\s+)?([a-zA-Z$_][\\w]*?)\\s*)\\(.*$");
+
+ private static final Pattern ENDING_OF_PARAMETERS_PATTERN = Pattern.compile("^(.*)\\)\\s*\\{.*$");
+
+ public static void applyWorkspaceEdit(WorkspaceEdit workspaceEdit, Editor editor, EclipseLanguageClient languageClient) {
+ Map changes = new HashMap<>();
+ applyWorkspaceEditInternal(workspaceEdit.getChanges(), changes, editor);
+ languageClient.notifyWatchedFilesChanged(new ArrayList<>(changes.values()));
+ }
+
+ public static void applyWorkspaceEdits(List workspaceEdits, Editor editor,
+ EclipseLanguageClient languageClient) {
+ if (workspaceEdits == null || workspaceEdits.isEmpty()) {
+ return;
+ }
+
+ Map changes = new HashMap<>();
+ for (WorkspaceEdit workspaceEdit : workspaceEdits) {
+ applyWorkspaceEditInternal(workspaceEdit.getChanges(), changes, editor);
+ }
+
+ languageClient.notifyWatchedFilesChanged(new ArrayList<>(changes.values()));
+ }
+
+ private static void applyWorkspaceEditInternal(Map> edits,
+ Map changes, Editor editor) {
+ if (edits == null || edits.isEmpty()) {
+ return;
+ }
+
+ for (Map.Entry> edit : edits.entrySet()) {
+ int i = edit.getKey().indexOf("src/main/java/");
+ String fileName = edit.getKey().substring(i);
+ if (editor.getContents().containsKey(fileName)) {
+ for (TextEdit textEdit : edit.getValue()) {
+ editor.replace(fileName, textEdit.getRange().getStart(), textEdit.getRange().getEnd(), textEdit.getNewText());
+ }
+ changes.putIfAbsent(fileName, new FileEvent(edit.getKey(), FileChangeType.Changed));
+ }
+ }
+ }
+
+ public static void applyTextEdits(String fileUri, List textEdits, Editor editor, EclipseLanguageClient languageClient) {
+ List changes = new ArrayList<>();
+ int i = fileUri.indexOf("src/main/java/");
+ String fileName = fileUri.substring(i);
+ if (editor.getContents().containsKey(fileName)) {
+ for (int j = textEdits.size() - 1; j >= 0; j--) {
+ TextEdit textEdit = textEdits.get(j);
+ editor.replace(fileName, textEdit.getRange().getStart(), textEdit.getRange().getEnd(), textEdit.getNewText());
+ }
+ FileEvent fileEvent = new FileEvent();
+ fileEvent.setUri(fileUri);
+ fileEvent.setType(FileChangeType.Changed);
+ changes.add(fileEvent);
+ }
+ languageClient.notifyWatchedFilesChanged(changes);
+ }
+
+ public static void deleteDirectory(File directoryToBeDeleted) {
+ File[] allContents = directoryToBeDeleted.listFiles();
+ if (allContents != null) {
+ for (File file : allContents) {
+ deleteDirectory(file);
+ }
+ }
+ directoryToBeDeleted.delete();
+ }
+
+ public static boolean isNullOrEmpty(CharSequence charSequence) {
+ return charSequence == null || charSequence.length() == 0;
+ }
+
+ public static boolean isNullOrEmpty(T[] array) {
+ return array == null || array.length == 0;
+ }
+
+ public static boolean isNullOrEmpty(Iterable iterable) {
+ return (iterable == null || !iterable.iterator().hasNext());
+ }
+
+ static void validateModifiers(int validTypeModifiers, int newModifiers) {
+ // 0 indicates no modifiers.
+ if (newModifiers == 0) {
+ return;
+ }
+
+ if (newModifiers < 0) {
+ throw new IllegalArgumentException("Modifiers aren't allowed to be less than or equal to 0.");
+ }
+
+ if (validTypeModifiers != (validTypeModifiers | newModifiers)) {
+ throw new IllegalArgumentException("Modifiers contain illegal modifiers for the type.");
+ }
+ }
+
+ /**
+ * Replaces the modifier for a given symbol.
+ *
+ * @param symbol The symbol having its modifier replaced.
+ * @param editor The editor containing information about the symbol.
+ * @param languageClient The language client handling replacement of the modifiers.
+ * @param replaceTarget A string regex that determines how the modifiers are replaced.
+ * @param modifierReplaceBase A string that determines the base modifier replacement.
+ * @param validaTypeModifiers The modifier bit flag used to validate the new modifiers.
+ * @param newModifiers The new modifiers for the symbol.
+ */
+ public static void replaceModifier(SymbolInformation symbol, Editor editor, EclipseLanguageClient languageClient,
+ String replaceTarget, String modifierReplaceBase, int validaTypeModifiers, int newModifiers) {
+ validateModifiers(validaTypeModifiers, newModifiers);
+
+ String fileUri = symbol.getLocation().getUri();
+ int i = fileUri.indexOf("src/main/java/");
+ String fileName = fileUri.substring(i);
+
+ int line = symbol.getLocation().getRange().getStart().getLine();
+ Position start = new Position(line, 0);
+ String oldLineContent = editor.getFileLine(fileName, line);
+ Position end = new Position(line, oldLineContent.length());
+
+ String newModifiersString = Modifier.toString(newModifiers);
+ String newLineContent = (isNullOrEmpty(newModifiersString))
+ ? oldLineContent.replaceFirst(replaceTarget, modifierReplaceBase)
+ : oldLineContent.replaceFirst(replaceTarget, newModifiersString + " " + modifierReplaceBase);
+
+ TextEdit textEdit = new TextEdit();
+ textEdit.setNewText(newLineContent);
+ textEdit.setRange(new Range(start, end));
+ WorkspaceEdit workspaceEdit = new WorkspaceEdit();
+ workspaceEdit.setChanges(Collections.singletonMap(fileUri, Collections.singletonList(textEdit)));
+ Utils.applyWorkspaceEdit(workspaceEdit, editor, languageClient);
+ }
+
+ public static S returnIfPresentOrThrow(Optional optional, Function returnFormatter,
+ Supplier orThrow) {
+ if (optional.isPresent()) {
+ return returnFormatter.apply(optional.get());
+ }
+
+ throw orThrow.get();
+ }
+
+ public static void writeLine(StringBuilder stringBuilder, String text) {
+ stringBuilder.append(text).append(System.lineSeparator());
+ }
+
+ /**
+ * Walks down the lines of a file until the line matches a predicate.
+ *
+ * @param editor The editor containing the file's information.
+ * @param fileName The name of the file.
+ * @param startLine The line to start walking.
+ * @param linePredicate The predicate that determines when a matching line is found.
+ * @return The first line that matches the predicate. If no line in the file matches the predicate {@code -1} is
+ * returned.
+ */
+ public static int walkDownFileUntilLineMatches(Editor editor, String fileName, int startLine,
+ Predicate linePredicate) {
+ return walkFileUntilLineMatches(editor, fileName, startLine, linePredicate, true);
+ }
+
+ /**
+ * Walks up the lines of a file until the line matches a predicate.
+ *
+ * @param editor The editor containing the file's information.
+ * @param fileName The name of the file.
+ * @param startLine The line to start walking.
+ * @param linePredicate The predicate that determines when a matching line is found.
+ * @return The first line that matches the predicate. If no line in the file matches the predicate {@code -1} is
+ * returned.
+ */
+ public static int walkUpFileUntilLineMatches(Editor editor, String fileName, int startLine,
+ Predicate linePredicate) {
+ return walkFileUntilLineMatches(editor, fileName, startLine, linePredicate, false);
+ }
+
+ private static int walkFileUntilLineMatches(Editor editor, String fileName, int startLine,
+ Predicate linePredicate, boolean isWalkingDown) {
+ int matchingLine = -1;
+
+ List fileLines = editor.getFileLines(fileName);
+ if (isWalkingDown) {
+ for (int line = startLine; line < fileLines.size(); line++) {
+ if (linePredicate.test(fileLines.get(line))) {
+ matchingLine = line;
+ break;
+ }
+ }
+ } else {
+ for (int line = startLine; line >= 0; line--) {
+ if (linePredicate.test(fileLines.get(line))) {
+ matchingLine = line;
+ break;
+ }
+ }
+ }
+
+ return matchingLine;
+ }
+
+ /**
+ * Utility method to add an annotation to a code block.
+ *
+ * @param annotation The annotation to add.
+ * @param customization The customization having an annotation added.
+ * @param refreshedCustomizationSupplier A supplier that returns a refreshed customization after the annotation is
+ * added.
+ * @param The type of the customization.
+ * @return A refreshed customization after the annotation was added.
+ */
+ public static T addAnnotation(String annotation, CodeCustomization customization,
+ Supplier refreshedCustomizationSupplier) {
+ SymbolInformation symbol = customization.getSymbol();
+ Editor editor = customization.getEditor();
+ String fileName = customization.getFileName();
+ String fileUri = customization.getFileUri();
+ EclipseLanguageClient languageClient = customization.getLanguageClient();
+
+ if (!annotation.startsWith("@")) {
+ annotation = "@" + annotation;
+ }
+
+ if (editor.getContents().containsKey(fileName)) {
+ int line = symbol.getLocation().getRange().getStart().getLine();
+ Position position = editor.insertBlankLine(fileName, line, true);
+ editor.replace(fileName, position, position, annotation);
+
+ FileEvent fileEvent = new FileEvent();
+ fileEvent.setUri(fileUri);
+ fileEvent.setType(FileChangeType.Changed);
+ languageClient.notifyWatchedFilesChanged(Collections.singletonList(fileEvent));
+
+ organizeImportsOnRange(languageClient, editor, fileUri, symbol.getLocation().getRange());
+ }
+
+ return refreshedCustomizationSupplier.get();
+ }
+
+ /**
+ * Removes the leading {@literal @} in an annotation name, if it exists.
+ *
+ * @param annotationName The annotation name.
+ * @return The annotation with any leading {@literal @} removed.
+ */
+ public static String cleanAnnotationName(String annotationName) {
+ return annotationName.startsWith("@") ? annotationName.substring(1) : annotationName;
+ }
+
+ /**
+ * Utility method to remove an annotation from a code block.
+ *
+ * @param codeCustomization The customization having an annotation removed.
+ * @param annotationRetriever Function that retrieves the potential annotation.
+ * @param refreshedCustomizationSupplier Supplier that returns a refreshed customization after the annotation is
+ * removed.
+ * @param The type of the customization.
+ * @return A refreshed customization if the annotation was removed, otherwise the customization as-is.
+ */
+ public static T removeAnnotation(T codeCustomization,
+ Function> annotationRetriever,
+ Supplier refreshedCustomizationSupplier) {
+ Editor editor = codeCustomization.getEditor();
+ String fileName = codeCustomization.getFileName();
+
+ CompilationUnit compilationUnit = StaticJavaParser.parse(editor.getFileContent(fileName));
+ Optional potentialAnnotation = annotationRetriever.apply(compilationUnit);
+
+ if (potentialAnnotation.isPresent()) {
+ potentialAnnotation.get().remove();
+ editor.replaceFile(fileName, compilationUnit.toString());
+ Utils.sendFilesChangeNotification(codeCustomization.getLanguageClient(), codeCustomization.getFileUri());
+ return refreshedCustomizationSupplier.get();
+ } else {
+ return codeCustomization;
+ }
+ }
+
+ /**
+ * Notifies watchers of a file that it has changed.
+ *
+ * @param languageClient The {@link EclipseLanguageClient} sending the file changed notification.
+ * @param fileUri The URI of the file that was changed.
+ */
+ public static void sendFilesChangeNotification(EclipseLanguageClient languageClient, String fileUri) {
+ FileEvent fileEvent = new FileEvent();
+ fileEvent.setUri(fileUri);
+ fileEvent.setType(FileChangeType.Changed);
+ languageClient.notifyWatchedFilesChanged(Collections.singletonList(fileEvent));
+ }
+
+ public static boolean declarationContainsSymbol(com.github.javaparser.Range declarationRange,
+ Range symbolRange) {
+ return declarationRange.begin.line <= symbolRange.getStart().getLine()
+ && declarationRange.end.line >= symbolRange.getStart().getLine();
+ }
+
+ /**
+ * Utility method to replace a body of a code block.
+ *
+ * @param newBody The new body.
+ * @param customization The customization having its body replaced.
+ * @param refreshedCustomizationSupplier A supplier that returns a refreshed customization after the body is
+ * replaced.
+ * @param The type of the customization.
+ * @return A refreshed customization after the body was replaced.
+ */
+ public static T replaceBody(String newBody, CodeCustomization customization,
+ Supplier refreshedCustomizationSupplier) {
+ SymbolInformation symbol = customization.getSymbol();
+ Editor editor = customization.getEditor();
+ String fileName = customization.getFileName();
+
+ int line = symbol.getLocation().getRange().getStart().getLine();
+ String methodBlockIndent = getIndent(editor.getFileLine(fileName, line));
+
+ // Loop until the line containing the body start is found.
+ Pattern startPattern = Pattern.compile(".*\\{\\s*");
+ int startLine = walkDownFileUntilLineMatches(editor, fileName, line, lineContent ->
+ startPattern.matcher(lineContent).matches()) + 1; // Plus one since the start is after the opening '{'
+
+ // Then determine the base indentation level for the body.
+ String methodContentIndent = getIndent(editor.getFileLine(fileName, startLine));
+ Position oldBodyStart = new Position(startLine, methodContentIndent.length());
+
+ // Then continue iterating over lines until the body close line is found.
+ Pattern closePattern = Pattern.compile(methodBlockIndent + "}\\s*");
+ int lastLine = walkDownFileUntilLineMatches(editor, fileName, startLine, lineContent ->
+ closePattern.matcher(lineContent).matches()) - 1; // Minus one since the end is before the closing '}'
+ Position oldBodyEnd = new Position(lastLine, editor.getFileLine(fileName, lastLine).length());
+
+ editor.replaceWithIndentedContent(fileName, oldBodyStart, oldBodyEnd, newBody, methodContentIndent.length());
+ FileEvent fileEvent = new FileEvent();
+ fileEvent.setUri(customization.getFileUri());
+ fileEvent.setType(FileChangeType.Changed);
+ customization.getLanguageClient().notifyWatchedFilesChanged(Collections.singletonList(fileEvent));
+
+ // Return the refreshed customization.
+ return refreshedCustomizationSupplier.get();
+ }
+
+ public static T replaceParameters(String newParameters,
+ CodeCustomization customization, Supplier refreshCustomizationSupplier) {
+ SymbolInformation symbol = customization.getSymbol();
+ Editor editor = customization.getEditor();
+ String fileName = customization.getFileName();
+ String fileUri = customization.getFileUri();
+ EclipseLanguageClient languageClient = customization.getLanguageClient();
+
+ // Beginning line of the symbol.
+ int line = symbol.getLocation().getRange().getStart().getLine();
+
+ // First find the starting location of the parameters.
+ // The beginning of the parameters may not be on the same line as the start of the signature.
+ Matcher matcher = BEGINNING_OF_PARAMETERS_PATTERN.matcher(editor.getFileLine(fileName, line));
+ while (!matcher.matches()) {
+ matcher = BEGINNING_OF_PARAMETERS_PATTERN.matcher(editor.getFileLine(fileName, ++line));
+ }
+
+ // Now that the line where the parameters begin is found create its position.
+ // Starting character is inclusive of the character offset, so add one as ')' isn't included in the capture.
+ Position parametersStart = new Position(line, matcher.group(1).length() + 1);
+
+ // Then find where the parameters end.
+ // The ending of the parameters may not be on the same line as the start of the parameters.
+ matcher = ENDING_OF_PARAMETERS_PATTERN.matcher(editor.getFileLine(fileName, line));
+ while (!matcher.matches()) {
+ matcher = ENDING_OF_PARAMETERS_PATTERN.matcher(editor.getFileLine(fileName, ++line));
+ }
+
+ // Now that the line where the parameters end is found gets create its position.
+ // Ending character is exclusive of the character offset.
+ Position parametersEnd = new Position(line, matcher.group(1).length());
+
+ editor.replace(fileName, parametersStart, parametersEnd, newParameters);
+
+ FileEvent fileEvent = new FileEvent();
+ fileEvent.setUri(fileUri);
+ fileEvent.setType(FileChangeType.Changed);
+ languageClient.notifyWatchedFilesChanged(Collections.singletonList(fileEvent));
+
+ return refreshCustomizationSupplier.get();
+ }
+
+ public static String getIndent(String content) {
+ Matcher matcher = INDENT_DETERMINATION_PATTERN.matcher(content);
+ return matcher.matches() ? matcher.group(1) : "";
+ }
+
+ /**
+ * Adds imports to the customization.
+ *
+ * @param importsToAdd Imports to add.
+ * @param customization Code customization to add imports.
+ * @param refreshCustomizationSupplier A supplier that returns a refreshed customization after the imports are
+ * added.
+ * @param Type of the customization.
+ * @return A refreshed customization.
+ */
+ public static T addImports(List importsToAdd,
+ ClassCustomization customization, Supplier refreshCustomizationSupplier) {
+ EclipseLanguageClient languageClient = customization.getLanguageClient();
+ Editor editor = customization.getEditor();
+ String fileUri = customization.getFileUri();
+ String fileName = customization.getFileName();
+
+ // Only add imports if they exist.
+ if (!isNullOrEmpty(importsToAdd)) {
+ // Always place imports after the package.
+ // The language server will format the imports once added, so location doesn't matter.
+ int importLine = Utils.walkDownFileUntilLineMatches(editor, fileName, 0,
+ line -> PACKAGE_PATTERN.matcher(line).matches()) + 1;
+
+ Position importPosition = new Position(importLine, 0);
+ String imports = importsToAdd.stream()
+ .map(importToAdd -> "import " + importToAdd + ";")
+ .collect(Collectors.joining("\n"));
+
+ editor.insertBlankLine(fileName, importLine, false);
+ editor.replace(fileName, importPosition, importPosition, imports);
+ }
+
+ FileEvent fileEvent = new FileEvent();
+ fileEvent.setUri(fileUri);
+ fileEvent.setType(FileChangeType.Changed);
+ languageClient.notifyWatchedFilesChanged(Collections.singletonList(fileEvent));
+
+ return refreshCustomizationSupplier.get();
+ }
+
+ public static void organizeImportsOnRange(EclipseLanguageClient languageClient, Editor editor, String fileUri,
+ Range range) {
+ languageClient.listCodeActions(fileUri, range, CodeActionKind.SourceOrganizeImports).stream()
+ .filter(ca -> ca.getKind().equals(CodeActionKind.SourceOrganizeImports))
+ .findFirst()
+ .ifPresent(action -> Utils.applyWorkspaceEdit(action.getEdit(), editor, languageClient));
+ }
+
+ public static boolean isWindows() {
+ String osName = System.getProperty("os.name");
+ return osName != null && osName.startsWith("Windows");
+ }
+
+ public static boolean isMac() {
+ String osName = System.getProperty("os.name");
+ return osName != null && (osName.startsWith("Mac") || osName.startsWith("Darwin"));
+ }
+
+ private Utils() {
+ }
+}
diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/EclipseLanguageClient.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/EclipseLanguageClient.java
new file mode 100644
index 0000000000..1b7e6da015
--- /dev/null
+++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/EclipseLanguageClient.java
@@ -0,0 +1,233 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.typespec.http.client.generator.core.customization.implementation.ls;
+
+import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.models.JavaCodeActionKind;
+import com.microsoft.typespec.http.client.generator.core.extension.jsonrpc.Connection;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import org.eclipse.lsp4j.ClientCapabilities;
+import org.eclipse.lsp4j.CodeAction;
+import org.eclipse.lsp4j.CodeActionCapabilities;
+import org.eclipse.lsp4j.CodeActionContext;
+import org.eclipse.lsp4j.CodeActionKind;
+import org.eclipse.lsp4j.CodeActionKindCapabilities;
+import org.eclipse.lsp4j.CodeActionLiteralSupportCapabilities;
+import org.eclipse.lsp4j.CodeActionParams;
+import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
+import org.eclipse.lsp4j.DocumentSymbolParams;
+import org.eclipse.lsp4j.FileEvent;
+import org.eclipse.lsp4j.InitializeParams;
+import org.eclipse.lsp4j.InitializeResult;
+import org.eclipse.lsp4j.Position;
+import org.eclipse.lsp4j.Range;
+import org.eclipse.lsp4j.RenameParams;
+import org.eclipse.lsp4j.ShowDocumentCapabilities;
+import org.eclipse.lsp4j.SymbolCapabilities;
+import org.eclipse.lsp4j.SymbolInformation;
+import org.eclipse.lsp4j.SymbolKind;
+import org.eclipse.lsp4j.SymbolKindCapabilities;
+import org.eclipse.lsp4j.TextDocumentClientCapabilities;
+import org.eclipse.lsp4j.TextDocumentIdentifier;
+import org.eclipse.lsp4j.TextEdit;
+import org.eclipse.lsp4j.WindowClientCapabilities;
+import org.eclipse.lsp4j.WindowShowMessageRequestActionItemCapabilities;
+import org.eclipse.lsp4j.WindowShowMessageRequestCapabilities;
+import org.eclipse.lsp4j.WorkspaceClientCapabilities;
+import org.eclipse.lsp4j.WorkspaceEdit;
+import org.eclipse.lsp4j.WorkspaceFolder;
+import org.eclipse.lsp4j.WorkspaceSymbolParams;
+import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler;
+import org.slf4j.Logger;
+
+import java.io.File;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+
+public class EclipseLanguageClient implements AutoCloseable {
+ private static final Gson GSON = new MessageJsonHandler(null).getDefaultGsonBuilder().create();
+
+ private static final Type LIST_SYMBOL_INFORMATION = createParameterizedType(List.class, SymbolInformation.class);
+ private static final Type LIST_CODE_ACTION = createParameterizedType(List.class, CodeAction.class);
+
+ private final EclipseLanguageServerFacade server;
+ private final Connection connection;
+ private final String workspaceDir;
+
+ public EclipseLanguageClient(String pathToLanguageServerPlugin, String workspaceDir, Logger logger) {
+ try {
+ this.workspaceDir = new File(workspaceDir).toURI().toString();
+ this.server = new EclipseLanguageServerFacade(pathToLanguageServerPlugin, logger);
+ this.connection = new Connection(server.getOutputStream(), server.getInputStream());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ if (!server.isAlive()) {
+ server.shutdown();
+ logger.error("Language server failed to start: " + server.getServerError());
+ throw new RuntimeException("Language server failed to start: " + server.getServerError());
+ }
+ }
+
+ public void initialize() {
+ int pid = (int) ProcessHandle.current().pid();
+
+ InitializeParams initializeParams = new InitializeParams();
+ initializeParams.setProcessId(pid);
+ initializeParams.setRootUri(workspaceDir);
+ initializeParams.setWorkspaceFolders(new ArrayList<>());
+ WorkspaceFolder workspaceFolder = new WorkspaceFolder();
+ workspaceFolder.setName("root");
+ workspaceFolder.setUri(workspaceDir);
+ initializeParams.getWorkspaceFolders().add(workspaceFolder);
+ initializeParams.setTrace("message");
+ initializeParams.setCapabilities(new ClientCapabilities());
+
+ // Configure window capabilities to disable everything as the server is run in headless mode.
+ WindowClientCapabilities windowClientCapabilities = new WindowClientCapabilities();
+ windowClientCapabilities.setWorkDoneProgress(false);
+ windowClientCapabilities.setShowDocument(new ShowDocumentCapabilities(false));
+ windowClientCapabilities.setShowMessage(new WindowShowMessageRequestCapabilities());
+ windowClientCapabilities.getShowMessage().setMessageActionItem(new WindowShowMessageRequestActionItemCapabilities(false));
+ initializeParams.getCapabilities().setWindow(windowClientCapabilities);
+
+ // Configure workspace capabilities to support workspace folders and all symbol kinds.
+ WorkspaceClientCapabilities workspaceClientCapabilities = new WorkspaceClientCapabilities();
+ workspaceClientCapabilities.setWorkspaceFolders(true);
+ workspaceClientCapabilities.setSymbol(new SymbolCapabilities(
+ new SymbolKindCapabilities(Arrays.asList(SymbolKind.values())), false));
+
+ // Configure text document capabilities to support code actions and all code action kinds.
+ List supportedCodeActions = new ArrayList<>(Arrays.asList(CodeActionKind.QuickFix,
+ CodeActionKind.Refactor, CodeActionKind.RefactorExtract, CodeActionKind.RefactorInline,
+ CodeActionKind.RefactorRewrite, CodeActionKind.Source, CodeActionKind.SourceOrganizeImports));
+ EnumSet.allOf(JavaCodeActionKind.class)
+ .forEach(javaCodeActionKind -> supportedCodeActions.add(javaCodeActionKind.toString()));
+ TextDocumentClientCapabilities textDocumentClientCapabilities = new TextDocumentClientCapabilities();
+ textDocumentClientCapabilities.setCodeAction(new CodeActionCapabilities(
+ new CodeActionLiteralSupportCapabilities(new CodeActionKindCapabilities(supportedCodeActions)), false));
+ initializeParams.getCapabilities().setTextDocument(textDocumentClientCapabilities);
+
+ sendRequest(connection, "initialize", initializeParams, InitializeResult.class);
+ connection.notifyWithSerializedObject("initialized", "null");
+ try {
+ Thread.sleep(2500);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void notifyWatchedFilesChanged(List changes) {
+ if (changes == null || changes.isEmpty()) {
+ return;
+ }
+
+ DidChangeWatchedFilesParams params = new DidChangeWatchedFilesParams(changes);
+ connection.notifyWithSerializedObject("workspace/didChangeWatchedFiles", GSON.toJson(params));
+ try {
+ // Wait for a moment as notify requests don't have a response. So, they're effectively fire and forget,
+ // which can result in some race conditions with customizations.
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public List findWorkspaceSymbol(String query) {
+ WorkspaceSymbolParams workspaceSymbolParams = new WorkspaceSymbolParams();
+ workspaceSymbolParams.setQuery(query);
+
+ return sendRequest(connection, "workspace/symbol", workspaceSymbolParams, LIST_SYMBOL_INFORMATION);
+ }
+
+ public List listDocumentSymbols(String fileUri) {
+ DocumentSymbolParams documentSymbolParams = new DocumentSymbolParams();
+ documentSymbolParams.setTextDocument(new TextDocumentIdentifier(fileUri));
+
+ return sendRequest(connection, "textDocument/documentSymbol", documentSymbolParams, LIST_SYMBOL_INFORMATION);
+ }
+
+ public WorkspaceEdit renameSymbol(String fileUri, Position symbolPosition, String newName) {
+ RenameParams renameParams = new RenameParams();
+ renameParams.setTextDocument(new TextDocumentIdentifier(fileUri));
+ renameParams.setPosition(symbolPosition);
+ renameParams.setNewName(newName);
+
+ return replaceTabsWithSpaces(sendRequest(connection, "textDocument/rename", renameParams, WorkspaceEdit.class));
+ }
+
+ public List listCodeActions(String fileUri, Range range, String codeActionKind) {
+ CodeActionContext context = new CodeActionContext(Collections.emptyList());
+ context.setOnly(Collections.singletonList(codeActionKind));
+ CodeActionParams codeActionParams = new CodeActionParams(new TextDocumentIdentifier(fileUri), range, context);
+
+ List codeActions = sendRequest(connection, "textDocument/codeAction", codeActionParams, LIST_CODE_ACTION);
+ for (CodeAction codeAction : codeActions) {
+ if (codeAction.getEdit() != null) {
+ continue;
+ }
+
+ if ("java.apply.workspaceEdit".equals(codeAction.getCommand().getCommand())) {
+ codeAction.setEdit(replaceTabsWithSpaces(
+ GSON.fromJson((JsonObject) codeAction.getCommand().getArguments().get(0), WorkspaceEdit.class)));
+ }
+ }
+
+ return codeActions;
+ }
+
+ private WorkspaceEdit replaceTabsWithSpaces(WorkspaceEdit workspaceEdit) {
+ if (workspaceEdit.getChanges() == null) {
+ return workspaceEdit;
+ }
+
+ for (List textEdits : workspaceEdit.getChanges().values()) {
+ for (TextEdit textEdit : textEdits) {
+ textEdit.setNewText(textEdit.getNewText().replace("\t", " "));
+ }
+ }
+
+ return workspaceEdit;
+ }
+
+ public void close() {
+ try {
+ connection.request("shutdown");
+ connection.notifyWithSerializedObject("exit", "null");
+ connection.stop();
+ server.shutdown();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static T sendRequest(Connection connection, String method, Object param, Type responseType) {
+ return GSON.fromJson(connection.requestWithSerializedObject(method, GSON.toJson(param)), responseType);
+ }
+
+ private static Type createParameterizedType(Type rawType, Type... typeArguments) {
+ return new ParameterizedType() {
+ @Override
+ public Type[] getActualTypeArguments() {
+ return typeArguments;
+ }
+
+ @Override
+ public Type getRawType() {
+ return rawType;
+ }
+
+ @Override
+ public Type getOwnerType() {
+ return null;
+ }
+ };
+ }
+}
diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/EclipseLanguageServerFacade.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/EclipseLanguageServerFacade.java
new file mode 100644
index 0000000000..5306ba9e86
--- /dev/null
+++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/EclipseLanguageServerFacade.java
@@ -0,0 +1,176 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.typespec.http.client.generator.core.customization.implementation.ls;
+
+import com.microsoft.typespec.http.client.generator.core.customization.implementation.Utils;
+import org.apache.tools.tar.TarEntry;
+import org.apache.tools.tar.TarInputStream;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.GZIPInputStream;
+
+public class EclipseLanguageServerFacade {
+ private static final String DOWNLOAD_BASE_URL
+ = "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/";
+
+ private final Process server;
+
+ public EclipseLanguageServerFacade(String pathToLanguageServerPlugin, Logger logger) {
+ Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown));
+ try {
+ int javaVersion = Runtime.version().feature();
+
+ Path languageServerPath = (pathToLanguageServerPlugin == null)
+ ? getLanguageServerDirectory(javaVersion, logger)
+ : Paths.get(pathToLanguageServerPlugin).resolve("jdt-language-server");
+
+ List command = new ArrayList<>();
+ command.add("java");
+ command.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044");
+ command.add("-Declipse.application=org.eclipse.jdt.ls.core.id1");
+ command.add("-Dosgi.bundles.defaultStartLevel=4");
+ command.add("-Declipse.product=org.eclipse.jdt.ls.core.product");
+ command.add("-Dlog.protocol=true");
+ command.add("-Dlog.level=ALL");
+ command.add("-noverify");
+ command.add("-Xmx1G");
+ command.add("-jar");
+
+ // This will need to get update when the target version of Eclipse language server changes.
+ if (javaVersion < 17) {
+ // JAR to start v1.12.0
+ command.add("./plugins/org.eclipse.equinox.launcher_1.6.400.v20210924-0641.jar");
+ } else if (javaVersion < 21) {
+ // JAR to start v1.29.0
+ command.add("./plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar");
+ } else {
+ // JAR to start v1.31.0
+ command.add("./plugins/org.eclipse.equinox.launcher_1.6.700.v20231214-2017.jar");
+ }
+
+ command.add("--add-modules=ALL-SYSTEM");
+ command.add("--add-opens java.base/java.util=ALL-UNNAMED");
+ command.add("--add-opens java.base/java.lang=ALL-UNNAMED");
+
+ command.add("-configuration");
+
+ if (Utils.isWindows()) {
+ command.add("./config_win");
+ } else if (Utils.isMac()) {
+ command.add("./config_mac");
+ } else {
+ command.add("./config_linux");
+ }
+
+ logger.info("Starting Eclipse JDT language server at {}", languageServerPath);
+ server = new ProcessBuilder(command)
+ .redirectOutput(ProcessBuilder.Redirect.PIPE)
+ .redirectInput(ProcessBuilder.Redirect.PIPE)
+ .redirectErrorStream(true)
+ .directory(languageServerPath.toFile())
+ .start();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static Path getLanguageServerDirectory(int javaVersion, Logger logger) throws IOException {
+ Path tmp = Paths.get(System.getProperty("java.io.tmpdir"));
+ Path autorestLanguageServer = tmp.resolve("autorest-java-language-server");
+
+ URL downloadUrl;
+ Path languageServerPath;
+ if (javaVersion < 17) {
+ // Eclipse JDT language server version 1.12.0 is the last version that supports Java 11, which is
+ // autorest.java's baseline.
+ downloadUrl = URI.create(DOWNLOAD_BASE_URL + "1.12.0/jdt-language-server-1.12.0-202206011637.tar.gz")
+ .toURL();
+ languageServerPath = autorestLanguageServer.resolve("1.12.0");
+ } else if (javaVersion < 21) {
+ // Eclipse JDT language server version 1.29.0 is the latest version that supports Java 17.
+ // In the future this else statement may need to be replaced with an else if as newer versions of
+ // Eclipse JDT language server may baseline on Java 21 (or later).
+ downloadUrl = URI.create(DOWNLOAD_BASE_URL + "1.29.0/jdt-language-server-1.29.0-202310261436.tar.gz")
+ .toURL();
+ languageServerPath = autorestLanguageServer.resolve("1.29.0");
+ } else {
+ // Eclipse JDT language server version 1.31.0 is the latest version that supports Java 21.
+ // In the future this else statement may need to be replaced with an else if as newer versions of
+ // Eclipse JDT language server may baseline on Java 25 (or later).
+ downloadUrl = URI.create(DOWNLOAD_BASE_URL + "1.31.0/jdt-language-server-1.31.0-202401111522.tar.gz")
+ .toURL();
+ languageServerPath = autorestLanguageServer.resolve("1.31.0");
+ }
+
+ Path languageServer = languageServerPath.resolve("jdt-language-server");
+ if (!Files.exists(languageServerPath) || !Files.exists(languageServer)) {
+ Files.createDirectories(languageServerPath);
+ Path zipPath = languageServerPath.resolve("jdt-language-server.tar.gz");
+ logger.info("Downloading Eclipse JDT language server from {} to {}", downloadUrl, zipPath);
+ try (InputStream in = downloadUrl.openStream()) {
+ Files.copy(in, zipPath);
+ }
+ logger.info("Downloaded Eclipse JDT language server to {}", zipPath);
+
+ return unzipLanguageServer(zipPath);
+ }
+
+ return languageServer;
+ }
+
+ private static Path unzipLanguageServer(Path zipPath) throws IOException {
+ try (TarInputStream tar = new TarInputStream(new GZIPInputStream(Files.newInputStream(zipPath)))) {
+ Path languageServerDirectory = zipPath.getParent().resolve("jdt-language-server");
+ Files.createDirectory(languageServerDirectory);
+ TarEntry entry;
+ while ((entry = tar.getNextEntry()) != null) {
+ if (entry.isDirectory()) {
+ Files.createDirectories(languageServerDirectory.resolve(entry.getName()));
+ } else {
+ Files.copy(tar, languageServerDirectory.resolve(entry.getName()));
+ }
+ }
+
+ return languageServerDirectory;
+ }
+ }
+
+ OutputStream getOutputStream() {
+ return server.getOutputStream();
+ }
+
+ InputStream getInputStream() {
+ return server.getInputStream();
+ }
+
+ boolean isAlive() {
+ return server.isAlive();
+ }
+
+ String getServerError() {
+ try {
+ return new String(server.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ }
+
+ public void shutdown() {
+ if (server != null && server.isAlive()) {
+ server.destroyForcibly();
+ }
+ }
+}
diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/models/JavaCodeActionKind.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/models/JavaCodeActionKind.java
new file mode 100644
index 0000000000..7c80e3a646
--- /dev/null
+++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/models/JavaCodeActionKind.java
@@ -0,0 +1,111 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.models;
+
+import org.eclipse.lsp4j.CodeActionKind;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public enum JavaCodeActionKind {
+ /**
+ * Base kind for "generate" source actions
+ */
+ SOURCE_GENERATE(CodeActionKind.Source + ".generate"),
+
+ /**
+ * Generate accessors kind
+ */
+ SOURCE_GENERATE_ACCESSORS(SOURCE_GENERATE + ".accessors"),
+
+ /**
+ * Generate hashCode/equals kind
+ */
+ SOURCE_GENERATE_HASHCODE_EQUALS(SOURCE_GENERATE + ".hashCodeEquals"),
+
+ /**
+ * Generate toString kind
+ */
+ SOURCE_GENERATE_TO_STRING(SOURCE_GENERATE + ".toString"),
+
+ /**
+ * Generate constructors kind
+ */
+ SOURCE_GENERATE_CONSTRUCTORS(SOURCE_GENERATE + ".constructors"),
+
+ /**
+ * Generate delegate methods
+ */
+ SOURCE_GENERATE_DELEGATE_METHODS(SOURCE_GENERATE + ".delegateMethods"),
+
+ /**
+ * Override/Implement methods kind
+ */
+ SOURCE_OVERRIDE_METHODS(CodeActionKind.Source + ".overrideMethods"),
+
+ /**
+ * Extract to method kind
+ */
+ REFACTOR_EXTRACT_METHOD(CodeActionKind.RefactorExtract + ".function"), // using `.function` instead of `.method` to match existing keybinding),
+
+ /**
+ * Extract to constant kind
+ */
+ REFACTOR_EXTRACT_CONSTANT(CodeActionKind.RefactorExtract + ".constant"),
+
+ /**
+ * Extract to variable kind
+ */
+ REFACTOR_EXTRACT_VARIABLE(CodeActionKind.RefactorExtract + ".variable"),
+
+ /**
+ * Extract to field kind
+ */
+ REFACTOR_EXTRACT_FIELD(CodeActionKind.RefactorExtract + ".field"),
+
+ /**
+ * Move kind
+ */
+ REFACTOR_MOVE(CodeActionKind.Refactor + ".move"),
+
+ /**
+ * Assign statement to new local variable
+ */
+ REFACTOR_ASSIGN_VARIABLE(CodeActionKind.Refactor + ".assign.variable"),
+
+ /**
+ * Assign statement to new field
+ */
+ REFACTOR_ASSIGN_FIELD(CodeActionKind.Refactor + ".assign.field"),
+
+ /**
+ * Base kind for "quickassist" code actions
+ */
+ QUICK_ASSIST("quickassist");
+
+ private static final Map STRING_TO_KIND_MAP;
+
+ static {
+ STRING_TO_KIND_MAP = new HashMap<>();
+
+ for (JavaCodeActionKind kind : JavaCodeActionKind.values()) {
+ STRING_TO_KIND_MAP.putIfAbsent(kind.value, kind);
+ }
+ }
+
+ private final String value;
+
+ JavaCodeActionKind(String value) {
+ this.value = value;
+ }
+
+ public static JavaCodeActionKind fromString(String value) {
+ return STRING_TO_KIND_MAP.get(value);
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+}
diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/FileUtils.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/FileUtils.java
new file mode 100644
index 0000000000..d9237240f0
--- /dev/null
+++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/FileUtils.java
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.typespec.http.client.generator.core.extension.base.util;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+/**
+ * Utility class for file operations.
+ */
+public final class FileUtils {
+ private FileUtils() {
+ }
+
+ /**
+ * Creates a temporary directory.
+ *
+ * If the environment setting {@code codegen.java.temp.directory} is set, the directory will be created under the
+ * specified path. Otherwise, the directory will be created under the system default temporary directory.
+ *
+ * {@link System#getProperty(String)} is checked before {@link System#getenv(String)}.
+ *
+ * If {@code codegen.java.temp.directory} is set to a non-existent path, the directory will be created under the
+ * system default temporary directory.
+ *
+ * @param prefix The prefix string to be used in generating the directory's name; may be {@code null}.
+ * @return The path to the newly created directory.
+ * @throws IOException If an I/O error occurs.
+ */
+ public static Path createTempDirectory(String prefix) throws IOException {
+ String tempDirectory = System.getProperty("codegen.java.temp.directory");
+ if (tempDirectory == null) {
+ tempDirectory = System.getenv("codegen.java.temp.directory");
+ }
+
+ if (tempDirectory != null) {
+ Path tempDirectoryPath = Paths.get(tempDirectory);
+ if (Files.exists(tempDirectoryPath)) {
+ return Files.createTempDirectory(tempDirectoryPath, prefix);
+ }
+ }
+
+ return Files.createTempDirectory(prefix);
+ }
+}
diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/HttpExceptionType.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/HttpExceptionType.java
new file mode 100644
index 0000000000..63a1db53c6
--- /dev/null
+++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/HttpExceptionType.java
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.typespec.http.client.generator.core.extension.base.util;
+
+/**
+ * Represents exception types for HTTP requests and responses.
+ */
+public enum HttpExceptionType {
+ /**
+ * The exception thrown when failing to authenticate the HTTP request with status code of {@code 4XX}, typically
+ * {@code 401 Unauthorized}.
+ *
+ *
A runtime exception indicating request authorization failure caused by one of the following scenarios:
+ *
+ *
A client did not send the required authorization credentials to access the requested resource, i.e.
+ * Authorization HTTP header is missing in the request
+ *
If the request contains the HTTP Authorization header, then the exception indicates that authorization
+ * has been refused for the credentials contained in the request header.
+ *
+ */
+ CLIENT_AUTHENTICATION,
+
+ /**
+ * The exception thrown when the HTTP request tried to create an already existing resource and received a status
+ * code {@code 4XX}, typically {@code 412 Conflict}.
+ */
+ RESOURCE_EXISTS,
+
+ /**
+ * The exception thrown for invalid resource modification with status code of {@code 4XX}, typically
+ * {@code 409 Conflict}.
+ */
+ RESOURCE_MODIFIED,
+
+ /**
+ * The exception thrown when receiving an error response with status code {@code 412 response} (for update) or
+ * {@code 404 Not Found} (for get/post).
+ */
+ RESOURCE_NOT_FOUND,
+
+ /**
+ * This exception thrown when an HTTP request has reached the maximum number of redirect attempts with a status code
+ * of {@code 3XX}.
+ */
+ TOO_MANY_REDIRECTS
+}
diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/JsonUtils.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/JsonUtils.java
new file mode 100644
index 0000000000..f5403b246e
--- /dev/null
+++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/JsonUtils.java
@@ -0,0 +1,95 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.typespec.http.client.generator.core.extension.base.util;
+
+import com.azure.json.JsonReader;
+import com.azure.json.JsonToken;
+import com.azure.json.WriteValueCallback;
+
+import java.io.IOException;
+import java.util.function.Supplier;
+
+/**
+ * Utility classes that help simplify repetitive code with {@code azure-json}.
+ */
+public final class JsonUtils {
+ /**
+ * Reads a JSON object from the passed {@code jsonReader} and creates an object of type {@code T} using the passed
+ * {@code objectCreator}. The {@code callback} is then called for each field in the JSON object to read the field
+ * value and set it on the object.
+ *
+ * @param jsonReader The JSON reader to read the object from.
+ * @param objectCreator The supplier that creates a new instance of the object.
+ * @param callback The callback that reads the field value and sets it on the object.
+ * @return The object created from the JSON object.
+ * @param The type of object to create.
+ * @throws IOException If an error occurs while reading the JSON object.
+ */
+ public static T readObject(JsonReader jsonReader, Supplier objectCreator, ReadObjectCallback callback)
+ throws IOException {
+ return jsonReader.readObject(reader -> {
+ T object = objectCreator.get();
+ fieldReaderLoop(reader, (fieldName, r) -> callback.read(object, fieldName, r));
+ return object;
+ });
+ }
+
+ /**
+ * Reads a JSON object from the passed {@code jsonReader} and creates an object of type {@code T} using the passed
+ * {@code objectCreator}. This method will skip the children of each field in the JSON object.
+ *
+ * @param jsonReader The JSON reader to read the object from.
+ * @param objectCreator The supplier that creates a new instance of the object.
+ * @return The object created from the JSON object.
+ * @param The type of object to create.
+ * @throws IOException If an error occurs while reading the JSON object.
+ */
+ public static T readEmptyObject(JsonReader jsonReader, Supplier objectCreator) throws IOException {
+ return jsonReader.readObject(reader -> {
+ T object = objectCreator.get();
+ fieldReaderLoop(reader, (fieldName, r) -> r.skipChildren());
+ return object;
+ });
+ }
+
+ /**
+ * Callback for reading a JSON object.
+ *
+ * @param The type of the object being read.
+ */
+ public interface ReadObjectCallback {
+ /**
+ * Reads a field from the JSON object and sets it on the object.
+ *
+ * @param object The object to set the field on.
+ * @param fieldName The name of the field being read.
+ * @param jsonReader The JSON reader to read the field value from.
+ * @throws IOException If an error occurs while reading the field value.
+ */
+ void read(T object, String fieldName, JsonReader jsonReader) throws IOException;
+ }
+
+ /**
+ * Helper method to iterate over the field of a JSON object.
+ *
+ * This method will reader the passed {@code jsonReader} until the end of the object is reached. For each field it
+ * will get the field name and iterate the reader to the next token. This method will then pass the field name and
+ * reader to the {@code fieldConsumer} for it to consume the JSON field as needed for the object being read.
+ *
+ * @param jsonReader The JSON reader to read the object from.
+ * @param fieldConsumer The consumer that will consume the field name and reader for each field in the object.
+ * @throws IOException If an error occurs while reading the JSON object.
+ */
+ public static void fieldReaderLoop(JsonReader jsonReader, WriteValueCallback fieldConsumer)
+ throws IOException {
+ while (jsonReader.nextToken() != JsonToken.END_OBJECT) {
+ String fieldName = jsonReader.getFieldName();
+ jsonReader.nextToken();
+
+ fieldConsumer.write(fieldName, jsonReader);
+ }
+ }
+
+ private JsonUtils() {
+ }
+}
diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/Connection.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/Connection.java
new file mode 100644
index 0000000000..13e019a6c3
--- /dev/null
+++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/Connection.java
@@ -0,0 +1,478 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.typespec.http.client.generator.core.extension.jsonrpc;
+
+import com.azure.json.JsonProviders;
+import com.azure.json.JsonReader;
+import com.azure.json.JsonWriter;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Represents a connection.
+ */
+public class Connection {
+ private OutputStream writer;
+ private PeekingBinaryReader reader;
+ private boolean isDisposed = false;
+ private final AtomicInteger requestId;
+ private final Map> tasks = new ConcurrentHashMap<>();
+ private final ExecutorService executorService = Executors.newCachedThreadPool();
+ private final CompletableFuture loop;
+ private final Map> dispatch = new ConcurrentHashMap<>();
+
+ /**
+ * Create a new Connection.
+ *
+ * @param writer The output stream to write to.
+ * @param input The input stream to read from.
+ */
+ public Connection(OutputStream writer, InputStream input) {
+ this.writer = writer;
+ this.reader = new PeekingBinaryReader(input);
+ this.loop = CompletableFuture.runAsync(this::listen);
+ this.requestId = new AtomicInteger(0);
+ }
+
+ private boolean isAlive = true;
+
+ /**
+ * Stops the connection.
+ */
+ public void stop() {
+ isAlive = false;
+ loop.cancel(true);
+ }
+
+ private String readJson() {
+ String jsonText = "";
+ while (true) {
+ try {
+ jsonText += reader.readAsciiLine(); // + "\n";
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot read JSON input");
+ }
+ try {
+ validateJsonText(jsonText);
+ return jsonText;
+ } catch (IOException e) {
+ // not enough text?
+ }
+ }
+ }
+
+ /**
+ * Tests that the passed {@code jsonText} is valid JSON.
+ *
+ * @param jsonText The JSON text to validate.
+ * @throws IOException If the JSON text is invalid.
+ */
+ private static void validateJsonText(String jsonText) throws IOException {
+ try (JsonReader jsonReader = JsonProviders.createReader(jsonText)) {
+ jsonReader.readUntyped();
+ }
+ }
+
+ /**
+ * Dispatches a message.
+ *
+ * @param path The path.
+ * @param method The method that gets the result as a JSON string.
+ */
+ public void dispatch(String path, Supplier method) {
+ dispatch.put(path, input -> {
+ String result = method.get();
+
+ return (result == null) ? "null" : result;
+ });
+ }
+
+ private static List readArguments(String input) {
+ try (JsonReader jsonReader = JsonProviders.createReader(input)) {
+ List ret = jsonReader.readArray(JsonReader::getString);
+ if (ret.size() == 2) {
+ // Return passed array if size is larger than 0, otherwise return a new ArrayList
+ return ret;
+ }
+
+ throw new RuntimeException("Invalid number of arguments");
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ }
+
+ /**
+ * Dispatches a notification.
+ *
+ * @param path The path.
+ * @param method The method.
+ */
+ public void dispatchNotification(String path, Runnable method) {
+ dispatch.put(path, input -> {
+ method.run();
+ return null;
+ });
+ }
+
+ /**
+ * Dispatches a message.
+ *
+ * @param path The path.
+ * @param method The method that gets the result as a JSON string.
+ */
+ public void dispatch(String path, BiFunction method) {
+ dispatch.put(path, input -> {
+ List args = readArguments(input);
+ return String.valueOf(method.apply(args.get(0), args.get(1)));
+ });
+ }
+
+ private String readJson(int contentLength) {
+ try {
+ return new String(reader.readBytes(contentLength), StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private boolean listen() {
+ while (isAlive) {
+ try {
+ int ch = reader.peekByte();
+ if (-1 == ch) {
+ // didn't get anything. start again, it'll know if we're shutting down
+ break;
+ }
+
+ if ('{' == ch || '[' == ch) {
+ // looks like a json block or array. let's do this.
+ // don't wait for this to finish!
+ process(readJson(), '{' == ch);
+
+ // we're done here, start again.
+ continue;
+ }
+
+ // We're looking at headers
+ Map headers = new HashMap<>();
+ String line = reader.readAsciiLine();
+ while (line != null && !line.isEmpty()) {
+ String[] bits = line.split(":", 2);
+ headers.put(bits[0].trim(), bits[1].trim());
+ line = reader.readAsciiLine();
+ }
+
+ ch = reader.peekByte();
+ // the next character had better be a { or [
+ if ('{' == ch || '[' == ch) {
+ String contentLengthStr = headers.get("Content-Length");
+ if (contentLengthStr != null && !contentLengthStr.isEmpty()) {
+ int contentLength = Integer.parseInt(contentLengthStr);
+ // don't wait for this to finish!
+ process(readJson(contentLength), '{' == ch);
+ continue;
+ }
+ // looks like a json block or array. let's do this.
+ // don't wait for this to finish!
+ process(readJson(), '{' == ch);
+ // we're done here, start again.
+ continue;
+ }
+
+ return false;
+
+ } catch (Exception e) {
+ if (!isAlive) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Processes a message.
+ *
+ * @param content The content.
+ * @param isObject Whether the JSON {@code content} is a JSON object.
+ */
+ public void process(String content, boolean isObject) {
+ // The only times this method is called is when the beginning portion of the JSON text is '{' or '['.
+ // So, instead of the previous design when using Jackson where a fully processed JsonNode was passed, use a
+ // simpler parameter 'isObject' to check if we are in a valid processing state.
+ if (!isObject) {
+ System.err.println("Unhandled: Batch Request");
+ return;
+ }
+
+ executorService.submit(() -> {
+
+ try (JsonReader jsonReader = JsonProviders.createReader(content)) {
+ Map jobject = jsonReader.readMap(reader -> {
+ if (reader.isStartArrayOrObject()) {
+ return reader.readChildren();
+ } else {
+ return reader.getString();
+ }
+ });
+
+ String method = jobject.get("method");
+ if (method != null) {
+ int id = processIdField(jobject.get("id"));
+
+ // this is a method call.
+ // pass it to the service that is listening...
+ if (dispatch.containsKey(method)) {
+ Function fn = dispatch.get(method);
+ String parameters = jobject.get("params");
+ String result = fn.apply(parameters);
+ if (id != -1) {
+ // if this is a request, send the response.
+ respond(id, result);
+ }
+ }
+ return;
+ }
+
+ if (jobject.containsKey("result")) {
+ String result = jobject.get("result");
+ int id = processIdField(jobject.get("id"));
+ if (id != -1) {
+ CompletableFuture f = tasks.remove(id);
+
+ try {
+ f.complete(result);
+ } catch (Exception e) {
+ f.completeExceptionally(e);
+ }
+ }
+ return;
+ }
+
+ String error = jobject.get("error");
+ if (error != null) {
+ int id = processIdField(jobject.get("id"));
+ if (id != -1) {
+ CompletableFuture f = tasks.remove(id);
+
+ try (JsonReader errorReader = JsonProviders.createReader(error)) {
+ Map errorObject = errorReader.readMap(JsonReader::readUntyped);
+
+ String message = String.valueOf(errorObject.get("message"));
+ Object dataField = errorObject.get("data");
+ if (dataField != null) {
+ message += " (" + dataField + ")";
+ }
+ f.completeExceptionally(new RuntimeException(message));
+ } catch (Exception e) {
+ f.completeExceptionally(e);
+ }
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ private static int processIdField(String idField) {
+ return (idField == null) ? -1 : Integer.parseInt(idField);
+ }
+
+ /**
+ * Closes the connection.
+ *
+ * @throws IOException If an I/O error occurs.
+ */
+ protected void close() throws IOException {
+ // ensure that we are in a cancelled state.
+ isAlive = false;
+ if (!isDisposed) {
+ // make sure we can't dispose twice
+ isDisposed = true;
+ tasks.forEach((ignored, future) -> future.cancel(true));
+
+ writer.close();
+ writer = null;
+ reader.close();
+ reader = null;
+ }
+ }
+
+ private void send(String text) {
+ byte[] data = text.getBytes(StandardCharsets.UTF_8);
+ byte[] header = ("Content-Length: " + data.length + "\r\n\r\n").getBytes(StandardCharsets.US_ASCII);
+ byte[] buffer = new byte[header.length + data.length];
+ System.arraycopy(header, 0, buffer, 0, header.length);
+ System.arraycopy(data, 0, buffer, header.length, data.length);
+ try {
+ writer.write(buffer);
+ writer.flush();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Sends an error.
+ *
+ * @param id The id.
+ * @param code The code.
+ * @param message The message.
+ */
+ public void sendError(int id, int code, String message) {
+ try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) {
+ jsonWriter.writeStartObject()
+ .writeStringField("jsonrpc", "2.0")
+ .writeIntField("id", id)
+ .writeStringField("message", message)
+ .writeStartObject("error")
+ .writeIntField("code", code)
+ .writeEndObject()
+ .writeEndObject()
+ .flush();
+
+ send(outputStream.toString(StandardCharsets.UTF_8));
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ }
+
+ /**
+ * Sends a response.
+ *
+ * @param id The id.
+ * @param value The value.
+ */
+ public void respond(int id, String value) {
+ try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) {
+ jsonWriter.writeStartObject()
+ .writeStringField("jsonrpc", "2.0")
+ .writeIntField("id", id)
+ .writeRawField("result", value)
+ .writeEndObject()
+ .flush();
+
+ send(outputStream.toString(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Sends a notification.
+ *
+ * @param methodName The method name.
+ * @param values The values.
+ */
+ public void notify(String methodName, Object... values) {
+ try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) {
+ jsonWriter.writeArray(values, JsonWriter::writeUntyped).flush();
+ notifyWithSerializedObject(methodName, outputStream.toString(StandardCharsets.UTF_8));
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ }
+
+ /**
+ * Sends a notification.
+ *
+ * @param methodName The method name.
+ * @param serializedObject The serialized object.
+ */
+ public void notifyWithSerializedObject(String methodName, String serializedObject) {
+ String json = (serializedObject == null)
+ ? "{\"jsonrpc\":\"2.0\",\"method\":\"" + methodName + "\"}"
+ : "{\"jsonrpc\":\"2.0\",\"method\":\"" + methodName + "\",\"params\":" + serializedObject + "}";
+
+ send(json);
+ }
+
+ /**
+ * Sends a request.
+ *
+ * @param methodName The method name.
+ * @param values The values.
+ * @return The result.
+ */
+ public String request(String methodName, Object... values) {
+ int id = requestId.getAndIncrement();
+ CompletableFuture response = new CompletableFuture<>();
+ tasks.put(id, response);
+
+ try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) {
+ jsonWriter.writeStartObject()
+ .writeStringField("jsonrpc", "2.0")
+ .writeStringField("method", methodName)
+ .writeIntField("id", id)
+ .writeArrayField("params", values, JsonWriter::writeUntyped)
+ .writeEndObject()
+ .flush();
+
+ send(outputStream.toString(StandardCharsets.UTF_8));
+
+ return response.get();
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Sends a request.
+ *
+ * @param method The method name.
+ * @param serializedObject The serialized object.
+ * @return The result.
+ */
+ public String requestWithSerializedObject(String method, String serializedObject) {
+ int id = requestId.getAndIncrement();
+ CompletableFuture response = new CompletableFuture<>();
+ tasks.put(id, response);
+
+ String json = (serializedObject == null)
+ ? "{\"jsonrpc\":\"2.0\",\"method\":\"" + method + "\",\"id\":" + id + "}"
+ : "{\"jsonrpc\":\"2.0\",\"method\":\"" + method + "\",\"id\":" + id + ",\"params\":" + serializedObject + "}";
+
+ send(json);
+ try {
+ return response.get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Waits for all requests to complete.
+ */
+ public void waitForAll() {
+ try {
+ loop.get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/PeekingBinaryReader.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/PeekingBinaryReader.java
new file mode 100644
index 0000000000..05ebd10565
--- /dev/null
+++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/PeekingBinaryReader.java
@@ -0,0 +1,71 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.typespec.http.client.generator.core.extension.jsonrpc;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+
+class PeekingBinaryReader implements Closeable {
+ Integer lastByte;
+ InputStream input;
+
+ PeekingBinaryReader(InputStream input) {
+ this.lastByte = null;
+ this.input = input;
+ }
+
+ int readByte() throws IOException {
+ if (lastByte != null) {
+ int result = lastByte;
+ lastByte = null;
+ return result;
+ }
+ return input.read();
+ }
+
+ int peekByte() throws IOException {
+ if (lastByte != null) {
+ return lastByte;
+ }
+ int result = readByte();
+ if (result != -1) {
+ lastByte = result;
+ }
+ return result;
+ }
+
+ byte[] readBytes(int count) throws IOException {
+ byte[] buffer = new byte[count];
+ int read = 0;
+ if (count > 0 && lastByte != null) {
+ buffer[read++] = (byte) (int) lastByte;
+ lastByte = null;
+ }
+ while (read < count) {
+ read += input.read(buffer, read, count - read);
+ }
+ return buffer;
+ }
+
+ String readAsciiLine() throws IOException {
+ StringBuilder result = new StringBuilder();
+ int c = readByte();
+ while (c != '\r' && c != '\n' && c != -1) {
+ result.append((char) c);
+ c = readByte();
+ }
+ if (c == '\r' && peekByte() == '\n') {
+ readByte();
+ }
+ if (c == -1 && result.length() == 0) {
+ return null;
+ }
+ return result.toString();
+ }
+
+ public void close() throws IOException {
+ input.close();
+ }
+}
diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/package-info.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/package-info.java
new file mode 100644
index 0000000000..d0a67b5808
--- /dev/null
+++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/package-info.java
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+/**
+ * Contains JSON RPC related classes.
+ */
+package com.microsoft.typespec.http.client.generator.core.extension.jsonrpc;
diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/Message.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/Message.java
new file mode 100644
index 0000000000..fb2e39a285
--- /dev/null
+++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/Message.java
@@ -0,0 +1,184 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.typespec.http.client.generator.core.extension.model;
+
+import com.azure.json.JsonReader;
+import com.azure.json.JsonSerializable;
+import com.azure.json.JsonToken;
+import com.azure.json.JsonWriter;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Represents a message.
+ */
+public class Message implements JsonSerializable {
+
+ /**
+ * Represents a message channel.
+ */
+ public MessageChannel channel;
+
+ /**
+ * Represents details.
+ */
+ public Object details;
+
+ /**
+ * Represents text.
+ */
+ public String text;
+
+ /**
+ * Represents a key.
+ */
+ public List key;
+
+ /**
+ * Represents a source location.
+ */
+ public List source;
+
+ /**
+ * Creates a new instance of the Message class.
+ */
+ public Message() {
+ }
+
+ /**
+ * Gets the source location of the message.
+ *
+ * @return The source location of the message.
+ */
+ public List getSource() {
+ return source;
+ }
+
+ /**
+ * Gets the key of the message.
+ *
+ * @return The key of the message.
+ */
+ public List getKey() {
+ return key;
+ }
+
+ /**
+ * Gets the details of the message.
+ *
+ * @return The details of the message.
+ */
+ public Object getDetails() {
+ return details;
+ }
+
+ /**
+ * Gets the channel of the message.
+ *
+ * @return The channel of the message.
+ */
+ public MessageChannel getChannel() {
+ return channel;
+ }
+
+ /**
+ * Gets the text of the message.
+ *
+ * @return The text of the message.
+ */
+ public String getText() {
+ return text;
+ }
+
+ /**
+ * Sets the channel of the message.
+ *
+ * @param channel The channel of the message.
+ */
+ public void setChannel(MessageChannel channel) {
+ this.channel = channel;
+ }
+
+ /**
+ * Sets the details of the message.
+ *
+ * @param details The details of the message.
+ */
+ public void setDetails(Object details) {
+ this.details = details;
+ }
+
+ /**
+ * Sets the key of the message.
+ *
+ * @param key The key of the message.
+ */
+ public void setKey(List key) {
+ this.key = key;
+ }
+
+ /**
+ * Sets the source location of the message.
+ *
+ * @param source The source location of the message.
+ */
+ public void setSource(List source) {
+ this.source = source;
+ }
+
+ /**
+ * Sets the text of the message.
+ *
+ * @param text The text of the message.
+ */
+ public void setText(String text) {
+ this.text = text;
+ }
+
+ @Override
+ public JsonWriter toJson(JsonWriter jsonWriter) throws IOException {
+ return jsonWriter.writeStartObject()
+ .writeStringField("Channel", channel == null ? null : channel.toString())
+ .writeUntypedField("Details", details)
+ .writeStringField("Text", text)
+ .writeArrayField("Key", key, JsonWriter::writeString)
+ .writeArrayField("Source", source, JsonWriter::writeJson)
+ .writeEndObject();
+ }
+
+ /**
+ * Deserializes a Message instance from the JSON data.
+ *
+ * @param jsonReader The JSON reader to deserialize from.
+ * @return A Message instance deserialized from the JSON data.
+ * @throws IOException If an error occurs during deserialization.
+ */
+ public static Message fromJson(JsonReader jsonReader) throws IOException {
+ return jsonReader.readObject(reader -> {
+ Message message = new Message();
+
+ while (reader.nextToken() != JsonToken.END_OBJECT) {
+ String fieldName = reader.getFieldName();
+ reader.nextToken();
+
+ if ("Channel".equals(fieldName)) {
+ message.channel = MessageChannel.valueOf(reader.getString());
+ } else if ("Details".equals(fieldName)) {
+ message.details = reader.readUntyped();
+ } else if ("Text".equals(fieldName)) {
+ message.text = reader.getString();
+ } else if ("Key".equals(fieldName)) {
+ message.key = reader.readArray(JsonReader::getString);
+ } else if ("Source".equals(fieldName)) {
+ message.source = reader.readArray(SourceLocation::fromJson);
+ } else {
+ reader.skipChildren();
+ }
+ }
+
+ return message;
+ });
+ }
+}
diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/MessageChannel.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/MessageChannel.java
new file mode 100644
index 0000000000..21048f9b8d
--- /dev/null
+++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/MessageChannel.java
@@ -0,0 +1,65 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.typespec.http.client.generator.core.extension.model;
+
+/**
+ * Represents a message channel.
+ */
+public enum MessageChannel {
+ /**
+ * Represents an information message.
+ */
+ INFORMATION("information"),
+
+ /**
+ * Represents a hint message.
+ */
+ HINT("hint"),
+
+ /**
+ * Represents a warning message.
+ */
+ WARNING("warning"),
+
+ /**
+ * Represents a debug message.
+ */
+ DEBUG("debug"),
+
+ /**
+ * Represents a verbose message.
+ */
+ VERBOSE("verbose"),
+
+ /**
+ * Represents an error message.
+ */
+ ERROR("error"),
+
+ /**
+ * Represents a fatal message.
+ */
+ FATAL("fatal"),
+
+ /**
+ * Represents a file message.
+ */
+ FILE("file"),
+
+ /**
+ * Represents a configuration message.
+ */
+ CONFIGURATION("configuration");
+
+ private final String value;
+
+ MessageChannel(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return this.value;
+ }
+}
diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/SmartLocation.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/SmartLocation.java
new file mode 100644
index 0000000000..9252a6f9de
--- /dev/null
+++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/SmartLocation.java
@@ -0,0 +1,76 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.typespec.http.client.generator.core.extension.model;
+
+import com.azure.json.JsonReader;
+import com.azure.json.JsonSerializable;
+import com.azure.json.JsonToken;
+import com.azure.json.JsonWriter;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Represents a smart location.
+ */
+public class SmartLocation implements JsonSerializable {
+ private List