Configuración del Entorno y Dependencias
Para implementar un editor de texto enriquecido en un sistema con separación estricta entre cliente y servidor, es necesario adaptar la integración de UEditor (de Baidu) hacia un enfoque API REST. A continuación, se detalla el proceso de adaptación del lado del servidor y la configuración requerida en el cliente.
- Preparación del Backend en Java
1.1. Archivo de Configuración (config.json)
Descargue el paquete completo de UEditor y localice el archivo config.json. Colóquelo dentro de una carpeta dedicada (por ejemplo, /src/main/webapp/conf/). Este archivo controla los parámetros de carga y validación. Es crucial recordar el valor asociado a imageActionName (por defecto, uploadimage), ya que se utilizará como clave en el controlador de recepción.
1.2. Adaptación del Código Fuente
Copie el paquete com.baidu.ueditor desde la distribución oficial hacia su proyecto Java. Ajuste las directivas de paquete en cada clase según la estructuar de su aplicación. Debido a que el archivo config.json reside en una ruta externa al classpath tradicional, es necesario modificar el método de lectura en ConfigManager.java para que apunte correctamente a la ubicación física en webapp.
1.3. Propiedades del Sistema
Defina los parámetros globales en un archivo de propiedades para evitar la codificación rígida. Ejemplo de configuración:
# URL base del servidor de archivos externo
file.server.base-url=http://192.168.1.50:8090/
# Ruta base para archivos multimedia del editor
editor.upload.directory=assets/editor-files/
# Directorios específicos por tipo
file.directory.images=images/
file.directory.videos=videos/
file.directory.documents=docs/
1.4. Utilidad de Transferencia de Archivos
A continuación, se presenta una versión refactorizada de la clase encargada de redirigir los archivos hacia un servidor independiente. Se ha reemplazado la dependencia de Jersey por java.net.http.HttpClient (disponible desde Java 11) para modernizar la lógica y reducir dependencias externas:
package com.app.core.utils;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import org.springframework.web.multipart.MultipartFile;
public class FileTransferUtil {
private static final HttpClient CLIENT = HttpClient.newHttpClient();
public static TransferResult pushToRemote(MultipartFile file, String baseUrl, String relativePath) throws IOException {
String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String uniqueId = UUID.randomUUID().toString().substring(0, 8);
String extension = getExtension(file.getOriginalFilename());
String newFileName = String.format("%s_%s.%s", timestamp, uniqueId, extension);
String relativeTarget = relativePath + newFileName;
String fullEndpoint = baseUrl + relativeTarget;
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(fullEndpoint))
.PUT(HttpRequest.BodyPublishers.ofByteArray(file.getBytes()))
.build();
try {
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200 || response.statusCode() == 201) {
return new TransferResult(true, newFileName, relativeTarget, fullEndpoint);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return new TransferResult(false, "", "", "");
}
private static String getExtension(String filename) {
int dotIndex = filename.lastIndexOf(".");
return (dotIndex == -1) ? "bin" : filename.substring(dotIndex + 1);
}
public static class TransferResult {
public boolean success;
public String newName;
public String relativePath;
public String absoluteUrl;
public TransferResult(boolean success, String newName, String relativePath, String absoluteUrl) {
this.success = success;
this.newName = newName;
this.relativePath = relativePath;
this.absoluteUrl = absoluteUrl;
}
}
}
1.5. Controlador de Recepción (RichTextAPI)
El endpoint principal debe manejar dos flujos distintos: la inyección de la configuración inicial (config) y la recepción de archivos binarios. Se utiliza un único mapeo para centralizar la lógica del editor:
package com.app.controller;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import com.app.core.utils.FileTransferUtil;
import com.app.ueditor.ActionEnter;
import com.fasterxml.jackson.databind.ObjectMapper;
@Controller
@RequestMapping("/api/editor")
public class RichTextController {
@Value("${file.server.base-url}")
private String remoteHost;
@Value("${editor.upload.directory}")
private String targetDirectory;
@ResponseBody
@RequestMapping(value = "/route", method = {RequestMethod.GET, RequestMethod.POST})
public void handleRequest(HttpServletRequest request, HttpServletResponse response,
@RequestParam(value = "action", required = false) String action) throws Exception {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
String contextPath = request.getServletContext().getRealPath("/");
// Flujo 1: Inyección de configuración
if ("config".equalsIgnoreCase(action)) {
String configJson = new ActionEnter(request, contextPath).exec();
try (PrintWriter out = response.getWriter()) {
out.print(configJson);
}
return;
}
// Flujo 2: Recepción de archivos multimedia
List<String> allowedActions = Arrays.asList("uploadimage", "uploadvideo", "uploadfile", "uploadscrawl");
boolean isUploadAction = allowedActions.stream().anyMatch(a -> a.equalsIgnoreCase(action));
if (isUploadAction) {
MultipartResolver resolver = new StandardServletMultipartResolver();
if (resolver.isMultipart(request)) {
javax.servlet.http.HttpServletRequest multipartHttpServletRequest = resolver.resolveMultipart(request);
Map<String, MultipartFile> files = multipartHttpServletRequest.getFileMap();
ObjectMapper mapper = new ObjectMapper();
for (MultipartFile uploadedFile : files.values()) {
FileTransferUtil.TransferResult result = FileTransferUtil.pushToRemote(uploadedFile, remoteHost, targetDirectory);
if (result.success) {
Map<String, Object> payload = Map.of(
"state", "SUCCESS",
"url", result.absoluteUrl,
"title", uploadedFile.getName(),
"original", uploadedFile.getOriginalFilename(),
"type", uploadedFile.getContentType(),
"size", uploadedFile.getSize()
);
try (PrintWriter out = response.getWriter()) {
out.print(mapper.writeValueAsString(payload));
}
} else {
Map<String, Object> error = Map.of("state", "ERROR: Fallo en la transferencia");
try (PrintWriter out = response.getWriter()) {
out.print(mapper.writeValueAsString(error));
}
}
}
}
}
}
}
- Adaptación del Cliente (Frontend)
2.1. Estructura de Archivos
Organice los recursos estáticos en una carpeta dedicada (ej. /static/ueditor/). Asegúrese de incluir ueditor.config.js, ueditor.all.min.js, ueditor.parse.js, el paquete de idiomas y las carpetas dialogs/, themes/ y third-party/.
2.2. Modificaciones Críticas en Scripts
- ueditor.config.js: Localice la propiedad
serverUrly asígnelle la ruta absoluta o relativa de su endpoint backend (ej.serverUrl: "/api/editor/route"). - Parche de Diálogos: Los archivos
dialogs/image/image.js,dialogs/video/video.jsydialogs/attachment/attachment.jsinyectan por defecto encabezados de autenticación personalizados que rompen la compatibilidad con servidores CORS estándar. Busque y elimine las líneas que establecen encabezados personalizados en la configuración del formulario o iframe de carga.
2.3. Implementación de Interfaz
El siguiente ejemplo muestra la carga asíncrona del editor y la captura del contenido HTML generado:
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Gestor de Contenido</title>
<script src="/js/jquery.min.js"></script>
<script src="/static/ueditor/ueditor.config.js"></script>
<script src="/static/ueditor/ueditor.all.min.js"></script>
<script src="/static/ueditor/lang/es-es/es-es.js"></script>
<style>
.editor-wrapper { margin: 20px 0; }
#action-btn { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; }
</style>
</head>
<body>
<h2>Redacción de Artículo</h2>
<div class="editor-wrapper">
<script id="rich-editor" type="text/plain" style="width:100%;height:400px;"></script>
</div>
<button id="action-btn">Publicar Contenido</button>
<script>
$(document).ready(function() {
const editorInstance = UE.getEditor('rich-editor', {
initialFrameWidth: '100%',
initialFrameHeight: 400,
autoHeightEnabled: false
});
$('#action-btn').on('click', function() {
const htmlContent = editorInstance.getContent();
if (!htmlContent.trim()) {
alert('El editor está vacío');
return;
}
$.ajax({
url: '/api/articles/create',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ body: htmlContent }),
success: (resp) => console.log('Guardado:', resp),
error: (err) => console.error('Error:', err)
});
});
});
</script>
</body>
</html>
2.4. Gestión de Contenido
La invocación a editorInstance.getContent() devuelve el HTML limpio generado por el usuario. Este fragmento puede enviarse directamente a un servicio de persistencia. Para que la carga de recursos multimedia funcione correctamente, el servidor de archivos secundario debe estar activo y configurado con permisos de escritura en el directorio especificado, así como las cabeceras CORS adecuadas para aceptar solicitudes desde el dominio principal.