diff --git a/bom/application/pom.xml b/bom/application/pom.xml index cc2b32cd589c..eeb019a09084 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -17,14 +17,14 @@ 1.12.488 3.10.1 2.2.0 - 1.14.12 + 1.14.15 1.17 1.70 - 3.0.0 + 4.0.0 4.33.0 0.14.1 - 2.16.1 - 2.22.1 + 2.17.2 + 2.27 22.3.3 @@ -1132,14 +1132,6 @@ - - - - javax.ws.rs - javax.ws.rs-api - 2.0.1 - - io.swagger.core.v3 @@ -1382,7 +1374,11 @@ pom import - + + org.glassfish.jersey.inject + jersey-hk2 + ${jersey.version} + jakarta.inject jakarta.inject-api @@ -1466,6 +1462,13 @@ junit junit 4.13.2 + + + org.hamcrest + hamcrest-core + + + @@ -1488,29 +1491,23 @@ awaitility ${awaitility.version} - - org.awaitility - awaitility-proxy - ${awaitility.version} - + org.mockito mockito-core - 5.11.0 + 5.12.0 org.mockito mockito-junit-jupiter - 5.11.0 + 5.12.0 test - org.hamcrest - hamcrest-all - 1.3 - + hamcrest + 2.1 + test diff --git a/dotCMS/pom.xml b/dotCMS/pom.xml index 57c5de5155bd..ce61a614fa49 100644 --- a/dotCMS/pom.xml +++ b/dotCMS/pom.xml @@ -831,6 +831,11 @@ org.yaml snakeyaml + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + compile + com.fasterxml.jackson.datatype jackson-datatype-guava @@ -975,13 +980,6 @@ - - - - javax.ws.rs - javax.ws.rs-api - compile - @@ -1223,6 +1221,11 @@ org.glassfish.jersey.media jersey-media-sse + + org.glassfish.jersey.inject + jersey-hk2 + + org.hamcrest - hamcrest-all + hamcrest compile diff --git a/dotCMS/src/main/java/com/dotcms/annotations/AllowNulls.java b/dotCMS/src/main/java/com/dotcms/annotations/AllowNulls.java new file mode 100644 index 000000000000..9fe3407f4c27 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/annotations/AllowNulls.java @@ -0,0 +1,16 @@ +package com.dotcms.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation is used in Immutables to allow null values in the generated classes collections + * See https://immutables.github.io/immutable.html#nulls-in-collection + */ +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface AllowNulls { + +} \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotcms/annotations/Nullable.java b/dotCMS/src/main/java/com/dotcms/annotations/Nullable.java new file mode 100644 index 000000000000..4f7c34f32b37 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/annotations/Nullable.java @@ -0,0 +1,16 @@ +package com.dotcms.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation is used in Immutables to allow null values in the generated classes collections + * See https://immutables.github.io/immutable.html#nulls-in-collection + */ +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Nullable { + +} \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotcms/annotations/SkipNulls.java b/dotCMS/src/main/java/com/dotcms/annotations/SkipNulls.java new file mode 100644 index 000000000000..cb5382b78f5a --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/annotations/SkipNulls.java @@ -0,0 +1,15 @@ +package com.dotcms.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +/** + * This annotation is used in Immutables to skip null values in the generated classes collections + * See https://immutables.github.io/immutable.html#nulls-in-collection + */ +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SkipNulls { + +} \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/portlet/PortletResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/portlet/PortletResource.java index 276123a1376b..af83c7fc7973 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/portlet/PortletResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/portlet/PortletResource.java @@ -110,16 +110,21 @@ public final Response saveNew(@Context final HttpServletRequest request, final Portlet contentPortlet = portletApi.findPortlet("content"); - final Map initValues = new HashMap<>(contentPortlet.getInitParams()); - initValues.put("name", formData.portletName); - initValues.put("baseTypes", formData.baseTypes); - initValues.put("contentTypes", formData.contentTypes); - initValues.put(DATA_VIEW_MODE_KEY, formData.dataViewMode); + final DotPortlet newPortlet = DotPortlet.builder() + .portletId(portletId) + .portletClass(contentPortlet.getPortletClass()) - final Portlet newPortlet = APILocator.getPortletAPI() - .savePortlet(new DotPortlet(portletId, contentPortlet.getPortletClass(), initValues), initData.getUser()); + .putInitParam("name", formData.portletName) + .putInitParam("baseTypes", formData.baseTypes) + .putInitParam("contentTypes", formData.contentTypes) + .putInitParam(DATA_VIEW_MODE_KEY, formData.dataViewMode) + .build(); - return Response.ok(new ResponseEntityView<>(Map.of(JSON_RESPONSE_PORTLET_ATTR, newPortlet.getPortletId()))).build(); + + final Portlet savedPortlet = APILocator.getPortletAPI() + .savePortlet(newPortlet.toPortlet(), initData.getUser()); + + return Response.ok(new ResponseEntityView<>(Map.of(JSON_RESPONSE_PORTLET_ATTR, savedPortlet.getPortletId()))).build(); } catch (final Exception e) { Logger.error(this, String.format("An error occurred when saving new Portlet with ID " + "'%s': %s", portletId, ExceptionUtil.getErrorMessage(e)), e); @@ -158,18 +163,24 @@ public final Response updatePortlet(@Context final HttpServletRequest request, f } final Portlet contentPortlet = portletApi.findPortlet("content"); - final Map initValues = new HashMap<>(contentPortlet.getInitParams()); - initValues.put("name", formData.portletName); - initValues.put("baseTypes", formData.baseTypes); - initValues.put("contentTypes", formData.contentTypes); - initValues.put(DATA_VIEW_MODE_KEY, formData.dataViewMode); + final DotPortlet updatedPortlet = DotPortlet.builder() + .portletId(portletId) + .portletClass(contentPortlet.getPortletClass()) + .putInitParam("name", formData.portletName) + .putInitParam("baseTypes", formData.baseTypes) + .putInitParam("contentTypes", formData.contentTypes) + .putInitParam(DATA_VIEW_MODE_KEY, formData.dataViewMode) + .build(); + final Portlet newPortlet = APILocator.getPortletAPI() - .savePortlet(new DotPortlet(portletId, contentPortlet.getPortletClass(), initValues), initData.getUser()); + .savePortlet(updatedPortlet.toPortlet(), initData.getUser()); return Response.ok(new ResponseEntityView<>(Map.of(JSON_RESPONSE_PORTLET_ATTR, newPortlet.getPortletId()))).build(); } catch (Exception e) { + Logger.error(this, String.format("An error occurred when updating Portlet with ID " + + "'%s': %s", formData.portletId, ExceptionUtil.getErrorMessage(e)), e); response = ResponseUtil.mapExceptionResponse(e); } diff --git a/dotCMS/src/main/java/com/dotmarketing/business/portal/DotPortlet.java b/dotCMS/src/main/java/com/dotmarketing/business/portal/DotPortlet.java index dc6e76d0c250..02f6edd8c20e 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/portal/DotPortlet.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/portal/DotPortlet.java @@ -1,193 +1,140 @@ package com.dotmarketing.business.portal; -import com.dotmarketing.util.UtilMethods; +import com.dotcms.annotations.SkipNulls; +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.*; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.liferay.portal.model.Portlet; +import org.immutables.value.Value; +import org.immutables.value.Value.Style.*; -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; -import java.util.ArrayList; -import java.util.HashMap; +import javax.xml.bind.annotation.*; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** - * This class implements the JAXB mapping for the portlet definitions in dotCMS. - * - * @author Jose Castro - * @since Jun 17th, 2024 + * This interface represents a DotPortlet, which is an immutable representation of a Portlet. + * It uses the Immutables framework for generating the implementation class and builder. + * Jackson annotations are used for JSON and XML serialization/deserialization. */ +@Value.Immutable +@Value.Style( + overshadowImplementation = true, + typeBuilder = "*Builder", + depluralize = true, + visibility = ImplementationVisibility.PACKAGE, + builderVisibility = BuilderVisibility.PACKAGE, + implementationNestedInBuilder = true, + deepImmutablesDetection = true, + jdkOnly = true +) +@JsonSerialize(as = DotPortlet.class) +@JsonDeserialize(builder = DotPortlet.Builder.class) +@JsonIgnoreProperties(ignoreUnknown = false) @XmlRootElement(name = "portlet") -public class DotPortlet extends Portlet { - - @XmlElement(name = "init-param") - private List initParams; +@XmlAccessorType(XmlAccessType.FIELD) +public interface DotPortlet extends XMLSerializable { /** - * Default class constructor, required by JAXB. + * Returns the portlet ID. + * @return The portlet ID as a String. */ - @SuppressWarnings("unused") - public DotPortlet() { - super(null, null, new HashMap<>()); - } + @XmlElement(name = "portlet-name") + @JsonProperty("portlet-name") + String getPortletId(); /** - * Creates an instance of this class based on the original/legacy definition or a dotCMS - * Portlet. - * - * @param portlet The original {@link Portlet} object. + * Returns the fully qualified class name of the portlet. + * @return The portlet class name as a String. */ - public DotPortlet(final Portlet portlet) { - this(portlet.getPortletId(), portlet.getPortletClass(), portlet.getInitParams()); - } + @XmlElement(name = "portlet-class") + @JsonProperty("portlet-class") + String getPortletClass(); /** - * Creates an instance of this class. - * - * @param portletId The ID of the portlet. - * @param portletClass The base class that handles the existing legacy Liferay/Struts logic to - * render the portlet. - * @param initParams The initialization and/or configuration parameters for the portlet. + * Returns a list of initialization parameters for the portlet. + * This method converts the internal Map representation to a List of InitParam objects. + * @return A List of InitParam objects. */ - public DotPortlet(String portletId, String portletClass, Map initParams) { - super(portletId, portletClass, initParams); - this.initParams = new ArrayList<>(); - for (Map.Entry entry : initParams.entrySet()) { - this.initParams.add(new InitParam(entry.getKey(), entry.getValue())); - } - } - - @Override - @XmlElement(name = "portlet-name") - public String getPortletId() { - return portletId; - } - - @Override - public void setPortletId(final String portletId) { - super.portletId = portletId; - } - - @Override - @XmlElement(name = "portlet-class") - public String getPortletClass() { - return portletClass; - } - - @Override - public void setPortletClass(final String portletClass) { - super.portletClass = portletClass; - } - - @SuppressWarnings("unused") @XmlElement(name = "init-param") - public List getInitParamList() { - return this.initParams; + @JsonGetter("init-param") + @JacksonXmlElementWrapper(useWrapping = false, localName = "init-params") + default List getInitParams() { + return initParams().entrySet().stream() + .map(e -> InitParam.of(e.getKey(), e.getValue())) + .collect(Collectors.toList()); } /** - * Returns the initialization parameters of the portlet as a map. By default, the JAXB mapping - * will read them as a list of attributes, but, for backward compatibility, we need to return - * them as mapped values. - * - * @return A map with the initialization parameters of the portlet. + * Returns the internal Map representation of initialization parameters. + * This method is ignored in JSON/XML serialization. + * @return A Map of initialization parameter names to values. */ - @Override - public Map getInitParams() { - if (UtilMethods.isSet(this.initParams)) { - for (final InitParam initParam : this.initParams) { - super.initParams.put(initParam.getName(), initParam.getValue()); - } - } - return super.getInitParams(); - } + @JsonIgnore + @XmlTransient + @SkipNulls + Map initParams(); - @Override - public String toString() { - return "DotPortlet{" + - " portletId='" + this.portletId + '\'' + - ", portletClass='" + this.portletClass + '\'' + - ", portletSource='" + this.portletSource + '\'' + - ", initParamsAsList=" + this.initParams + - '}'; + /** + * Creates a DotPortlet instance from a Portlet object. + * @param portlet The Portlet object to convert. + * @return A new DotPortlet instance. + */ + static DotPortlet from(Portlet portlet) { + return DotPortlet.builder() + .portletId(portlet.getPortletId()) + .portletClass(portlet.getPortletClass()) + .initParams(portlet.getInitParams()) + .build(); } /** - * Represents the initialization parameter tags in a Portlet definition. It can contain any - * value/pair required by the Portlet, there's no technical limitation. + * Converts this DotPortlet instance to a Portlet object. + * @return A new Portlet instance. */ - @XmlAccessorType(XmlAccessType.FIELD) - public static class InitParam { - - @XmlElement(name = "name") - private String name; - - @XmlElement(name = "value") - private String value; - - /** - * Default class constructor, required by JAXB. - */ - @SuppressWarnings("unused") - public InitParam() { - } - - /** - * Creates an instance of an initialization parameter. - * - * @param name The name of the parameter. - * @param value The value of the parameter. - */ - public InitParam(final String name, final String value) { - this.name = name; - this.value = value; - } - - /** - * Returns the name of the parameter. - * - * @return The name of the parameter. - */ - public String getName() { - return this.name; - } + default Portlet toPortlet() { + return new Portlet(getPortletId(), getPortletClass(), initParams()); + } - /** - * Sets the name of the parameter. - * - * @param name The name of the parameter. - */ - public void setName(String name) { - this.name = name; - } + /** + * Builder class for DotPortlet. + * This class is generated by the Immutables framework and extended here for custom functionality. + */ + class Builder extends DotPortletBuilder implements XMLEnabledBuilder { /** - * Returns the value of the parameter. - * - * @return The value of the parameter. + * Adds a list of InitParam objects to the builder. + * This method converts the List to the internal Map representation. + * @param initParams A List of InitParam objects. + * @return This Builder instance for method chaining. */ - public String getValue() { - return this.value; + @XmlElement(name = "init-param") + @JsonSetter("init-param") + @JacksonXmlElementWrapper(useWrapping = false, localName = "init-params") + public Builder addAllInitParams(List initParams) { + putAllInitParams(initParams.stream() + .collect(Collectors.toMap(InitParam::getName, InitParam::getValue))); + return this; } /** - * Sets the value of the parameter. - * - * @param value The value of the parameter. + * Creates a Builder instance from a Portlet object. + * @param portlet The Portlet object to convert. + * @return This Builder instance for method chaining. */ - public void setValue(String value) { - this.value = value; - } - - @Override - public String toString() { - return "InitParam{" + - "name='" + this.name + '\'' + - ", value='" + this.value + '\'' + - '}'; + public Builder from(Portlet portlet) { + return portletId(portlet.getPortletId()) + .portletClass(portlet.getPortletClass()) + .initParams(portlet.getInitParams()); } + } - } - -} + /** + * Creates a new Builder instance for DotPortlet. + * @return A new Builder instance. + */ + static Builder builder() { + return new Builder(); + } +} \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotmarketing/business/portal/InitParamTuple.java b/dotCMS/src/main/java/com/dotmarketing/business/portal/InitParamTuple.java new file mode 100644 index 000000000000..dc583d288efb --- /dev/null +++ b/dotCMS/src/main/java/com/dotmarketing/business/portal/InitParamTuple.java @@ -0,0 +1,36 @@ +package com.dotmarketing.business.portal; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import org.immutables.value.Value; +import org.immutables.value.Value.Style.ImplementationVisibility; + +@Value.Immutable +@Value.Style( + typeAbstract = "*Tuple", + typeImmutable = "*", + visibility = ImplementationVisibility.PUBLIC, + allParameters = true, + defaults = @Value.Immutable(builder = false, copy = false) +) +@JsonSerialize(as = InitParam.class) +@JsonDeserialize(as = InitParam.class) +@JsonIgnoreProperties(ignoreUnknown = true) +@XmlRootElement(name = "init-param") +@XmlAccessorType(XmlAccessType.FIELD) +public interface InitParamTuple { + + @XmlElement(name = "name") + @JsonProperty("name") + String getName(); + + @XmlElement(name = "value") + @JsonProperty("value") + String getValue(); +} \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotmarketing/business/portal/PortletFactory.java b/dotCMS/src/main/java/com/dotmarketing/business/portal/PortletFactory.java index 1aef58fb9155..77ab1dc7f56a 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/portal/PortletFactory.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/portal/PortletFactory.java @@ -99,7 +99,7 @@ public interface PortletFactory { Map xmlToPortlets(InputStream[] xmlFiles) throws SystemException; /** - * Transforms the specified XML string into a DotPortlet object. This allows the API to read + * Transforms the specified XML string into a Portlet object. This allows the API to read * portlets that are defined in configuration files or the database. * * @param xml The XML string to transform. @@ -110,6 +110,6 @@ public interface PortletFactory { * @throws JAXBException An error occurred when mapping the XML String to a {@link DotPortlet} * object. */ - Optional xmlToPortlet(final String xml) throws IOException, JAXBException; + Optional xmlToPortlet(final String xml) throws IOException, JAXBException; } diff --git a/dotCMS/src/main/java/com/dotmarketing/business/portal/PortletFactoryImpl.java b/dotCMS/src/main/java/com/dotmarketing/business/portal/PortletFactoryImpl.java index c70dc4ea3ec2..aebf2ebfc0e6 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/portal/PortletFactoryImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/portal/PortletFactoryImpl.java @@ -1,390 +1,301 @@ -package com.dotmarketing.business.portal; - -import com.dotcms.api.system.event.Payload; -import com.dotcms.api.system.event.SystemEventType; -import com.dotcms.business.CloseDBIfOpened; -import com.dotcms.business.WrapInTransaction; -import com.dotcms.exception.ExceptionUtil; -import com.dotmarketing.business.APILocator; -import com.dotmarketing.business.CacheLocator; -import com.dotmarketing.common.db.DotConnect; -import com.dotmarketing.exception.DotDataException; -import com.dotmarketing.exception.DotRuntimeException; -import com.dotmarketing.util.Logger; -import com.dotmarketing.util.UtilMethods; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.Lists; -import com.liferay.portal.SystemException; -import com.liferay.portal.ejb.PrincipalBean; -import com.liferay.portal.model.Portlet; -import com.liferay.util.FileUtil; -import io.vavr.control.Try; - -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBException; -import javax.xml.bind.Marshaller; -import javax.xml.bind.UnmarshalException; -import javax.xml.bind.Unmarshaller; -import java.io.ByteArrayInputStream; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import static java.nio.charset.StandardCharsets.UTF_8; - -/** - * Implementation class for the {@link PortletFactory} interface. This class uses JAXB to map XML - * files into Java objects. - *

By default, portlet definitions come from three sources: - *

    - *
  • The {@code "/WEB-INF/portlet.xml"} file.
  • - *
  • The {@code "/WEB-INF/portlet-ext.xml"} file.
  • - *
  • The database, where custom Portlets are stored.
  • - *
- *

- * - * @author Erick Gonzalez - * @since Apr 24th, 2019 - */ -public class PortletFactoryImpl extends PrincipalBean implements PortletFactory { - - private final String[] systemXmlFiles; - - private static final Map, JaxbContext> jaxbContexts; - - // Creates the JAXB contexts for the {@link DotPortlet} and {@link PortletList} classes. They're - // used by JAXB to map the contents of the configuration XML files into such classes. - static { - try { - jaxbContexts = Map.of( - DotPortlet.class, new JaxbContext(DotPortlet.class), - PortletList.class, new JaxbContext(PortletList.class)); - } catch (final JAXBException e) { - throw new DotRuntimeException(String.format("FATAL - Failed to create JAXB contexts: " + - "%s", ExceptionUtil.getErrorMessage(e)), e); - } - } - - /** - * Creates an instance of this Factory using the specified XML files as sources of Portlet - * definitions. - * - * @param systemXmlFiles The paths to the XML files to read. - */ - public PortletFactoryImpl(final String[] systemXmlFiles) { - this.systemXmlFiles = systemXmlFiles; - } - - /** - * Default class constructor. It sets the default paths to the system XML files. - */ - public PortletFactoryImpl() { - this(new String[] {FileUtil.getRealPath("/WEB-INF/portlet.xml"), FileUtil.getRealPath("/WEB-INF/portlet-ext.xml")}); - } - - /** - * Reads the contents of the specified XML file, and maps the Portlet definitions from it into - * Portlet objects. - * - * @param pathToXmlFile The path to the XML file to read. - * - * @return A map with the {@link Portlet} definitions. - * - * @throws IOException An error occurred while reading the XML file. - * @throws JAXBException An error occurred while mapping the XML file into Java objects. - */ - private Map xmlToPortlets(final String pathToXmlFile) throws IOException, JAXBException { - if (UtilMethods.isNotSet(pathToXmlFile)) { - return new HashMap<>(); - } - Logger.debug(this, "Loading Portlets from file: " + pathToXmlFile); - final InputStream stream = new FileInputStream(pathToXmlFile); - return xmlToPortlets(stream); - } - - /** - * Takes the Input Stream of an XML file and extracts the Portlet definitions from it into - * Portlet objects. - * - * @param fileStream The {@link InputStream} of the XML file to read. - * - * @return A map with the {@link Portlet} definitions. - * - * @throws JAXBException An error occurred while mapping the XML file into Java objects. - */ - private Map xmlToPortlets(final InputStream fileStream) throws JAXBException { - if (null == fileStream) { - return new HashMap<>(); - } - final Map portlets = new HashMap<>(); - final PortletList portletList = (PortletList) jaxbContexts.get(PortletList.class).unmarshall(fileStream); - int counter = 1; - if (UtilMethods.isSet(portletList) && UtilMethods.isSet(portletList.getPortlets())) { - for (final DotPortlet portlet : portletList.getPortlets()) { - portlets.put(portlet.getPortletId(), portlet); - Logger.debug(this, String.format("%d. Loading portlet ID '%s'", counter, portlet.getPortletId())); - counter++; - } - } - return portlets; - } - - @Override - @VisibleForTesting - public Optional xmlToPortlet(final String xml) throws JAXBException { - final InputStream stream = new ByteArrayInputStream(xml.getBytes(UTF_8)); - final DotPortlet portlet = (DotPortlet) jaxbContexts.get(DotPortlet.class).unmarshall(stream); - if (null == portlet.getPortletId() || null == portlet.getPortletClass()) { - return Optional.empty(); - } - return Optional.of(portlet); - } - - @Override - public Map xmlToPortlets(final String[] xmlFiles) throws SystemException { - final Map portlets = new HashMap<>(); - for (final String xmlFile : xmlFiles) { - try { - portlets.putAll(xmlToPortlets(xmlFile)); - } catch (final Exception e) { - throw new SystemException(String.format("An error occurred when loading portlets from XML file " + - "'%s': %s", xmlFile, ExceptionUtil.getErrorMessage(e)), e); - } - } - return portlets; - } - - @Override - public Map xmlToPortlets(final InputStream[] xmlFiles) throws SystemException { - final Map portlets = new HashMap<>(); - for (final InputStream xmlFile : xmlFiles) { - try { - portlets.putAll(xmlToPortlets(xmlFile)); - } catch (final Exception e) { - throw new SystemException(String.format("An error occurred when loading portlets from XML stream: " + - "%s", ExceptionUtil.getErrorMessage(e)), e); - } - } - return portlets; - } - - @WrapInTransaction - @Override - public void deletePortlet(final String portletId) throws DotDataException { - final DotConnect db = new DotConnect(); - db.setSQL("delete from portletpreferences where portletid=?").addParam(portletId).loadResult(); - db.setSQL("delete from portlet where portletid=?").addParam(portletId).loadResult(); - db.setSQL("delete from cms_layouts_portlets where portlet_id=?" ).addParam(portletId).loadResult(); - CacheLocator.getPortletCache().clearCache(); - CacheLocator.getLayoutCache().clearCache(); - APILocator.getSystemEventsAPI().pushAsync(SystemEventType.UPDATE_PORTLET_LAYOUTS, new Payload()); - } - - @Override - public Portlet findById(final String portletId) { - return getPortletMap().get(portletId); - } - - /** - * Loads the dotCMS portlets from both the system XML files and the database. - * - * @return A map with the portlets loaded from the system XML files and the database. - */ - private Map loadSystemPortlets() { - final Map portlets = new HashMap<>(); - - try { - portlets.putAll(xmlToPortlets(this.systemXmlFiles)); - } catch (final Exception e) { - Logger.error(this, e.getMessage(), e); - } - - final List portletList = Try.of(this::findAllDb).getOrElse(Lists.newArrayList()); - for (final Portlet portlet : portletList) { - portlets.put(portlet.getPortletId(), portlet); - } - return portlets; - } - - /** - * Returns the available Portlets in the current dotCMS instance. If the Portlets are not loaded - * in cache yet, it takes care of doing so. - * - * @return A map with the available Portlets. - */ - private Map getPortletMap() { - final PortletCache cache = new PortletCache(); - Map portletsMap = cache.getAllPortlets(); - if (portletsMap.isEmpty()) { - synchronized (PortletCache.class) { - portletsMap = cache.getAllPortlets(); - if (portletsMap.isEmpty()) { - final Map portletMap = this.loadSystemPortlets(); - portletsMap.putAll(portletMap); - cache.putAllPortlets(portletsMap); - } - } - } - return portletsMap; - } - - @Override - public Collection getPortlets() throws SystemException { - return getPortletMap().values(); - } - - /** - * Loads all Portlets directly from the database. Keep in mind that Users can create their own - * portlets for displaying different contents of a given type; e.g., Videos, PDFs, etc. - * - * @return A list with all the Portlets stored in the database. - * - * @throws DotDataException An error occurred while loading the Portlets from the database. - */ - @CloseDBIfOpened - public List findAllDb() throws DotDataException { - final DotConnect db = new DotConnect(); - db.setSQL("select * from portlet where companyid=?").addParam("dotcms.org"); - final List portlets = new ArrayList<>(); - final List> portletsFromDb = db.loadObjectResults(); - for (final Map portletData : portletsFromDb) { - final Portlet testPortlet = new Portlet((String) portletData.get("portletid"), (String) portletData.get("groupid"), (String) portletData.get("companyid"), - (String) portletData.get("defaultpreferences"), false, (String) portletData.get("roles"), true); - - try { - final Optional xmlPortlet = xmlToPortlet((String) portletData.get("defaultpreferences")); - xmlPortlet.ifPresent(portlets::add); - } catch (final UnmarshalException e) { - Logger.debug(this.getClass(), String.format("XML code for Portlet ID '%s'" + - " cannot be mapped to DotPortlet class: %s", testPortlet.getPortletId(), ExceptionUtil.getErrorMessage(e))); - } catch (final Exception e) { - Logger.warn(this.getClass(), String.format("Unable to parse XML code to Portlet with ID '%s': %s", - testPortlet.getPortletId(), ExceptionUtil.getErrorMessage(e))); - } - } - return portlets; - } - - @WrapInTransaction - @Override - public Portlet insertPortlet(final Portlet portlet) throws DotDataException { - if (doesPortletExistInDb(portlet)) { - return updatePortlet(portlet); - } - final String portletXML = portletToXml(portlet); - new DotConnect().setSQL( - "insert into portlet (portletid, groupid, companyid, defaultpreferences, narrow, active_) values(?,?,?,?,?,?)") - .addParam(portlet.getPortletId()) - .addParam(UtilMethods.isSet(portlet.getGroupId())?portlet.getGroupId():"SHARED_KEY") - .addParam(portlet.getCompanyId()) - .addParam(portletXML) - .addParam(portlet.getNarrow()) - .addParam(portlet.getActive()) - .loadResult(); - new PortletCache().clear(); - return portlet; - } - - @WrapInTransaction - @Override - public Portlet updatePortlet(final Portlet portlet) throws DotDataException { - if (!doesPortletExistInDb(portlet)) { - return insertPortlet(portlet); - } - final String portletXML = portletToXml(portlet); - final DotConnect db = new DotConnect(); - db.setSQL("update portlet set groupid=?, defaultpreferences=? where portletid=?") - .addParam(portlet.getGroupId()) - .addParam(portletXML) - .addParam(portlet.getPortletId()) - .loadResult(); - new PortletCache().clear(); - return portlet; - } - - /** - * Checks whether the specified Portlet already exists in the database or not. - * - * @param portlet The {@link Portlet} to check. - * - * @return If the specified Portlet already exists, returns {@code true}. - */ - @CloseDBIfOpened - private boolean doesPortletExistInDb(final Portlet portlet) { - return (new DotConnect().setSQL("select count(*) as test from portlet where portletid=?") - .addParam(portlet.getPortletId()) - .getInt("test") > 0); - } - - @Override - public String portletToXml(final Portlet portlet) throws DotDataException { - try { - final StringWriter sw = new StringWriter(); - final Marshaller jaxbMarshaller = jaxbContexts.get(DotPortlet.class).getMarshaller(); - jaxbMarshaller.marshal(new DotPortlet(portlet), sw); - return sw.toString(); - } catch (final JAXBException e) { - Logger.error(this, String.format("Failed to transform the Portlet with ID " + - "'%s' into XML: %s", portlet.getPortletId(), ExceptionUtil.getErrorMessage(e))); - throw new DotDataException(e); - } - } - - /** - * A simple class to hold the JAXB context and its respective Marshaller and Unmarshaller - * implementations for a specific class. - */ - private static class JaxbContext { - - final JAXBContext jaxbContextObj; - final Unmarshaller unmarshaller; - final Marshaller marshaller; - - /** - * Default class constructor. - * - * @param clazz The class to create the JAXB context, marshaller and unmarshaller for. - * - * @throws JAXBException An error occurred while creating the JAXB context. - */ - public JaxbContext(final Class clazz) throws JAXBException { - this.jaxbContextObj = JAXBContext.newInstance(clazz); - this.marshaller = jaxbContextObj.createMarshaller(); - this.marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); - this.unmarshaller = jaxbContextObj.createUnmarshaller(); - } - - /** - * Unmarshalls the specified Input Stream. That is, converts XML data into a java content - * tree. - * - * @param fileStream The {@link InputStream} to unmarshall. - * - * @return The unmarshalled object. - * - * @throws JAXBException An error occurred while unmarshalling the Input Stream. - */ - public Object unmarshall(final InputStream fileStream) throws JAXBException { - return unmarshaller.unmarshal(fileStream); - } - - /** - * Returns the {@code Marshaller} object that can be used to convert a java content tree - * into XML data. - * - * @return The {@code Marshaller} object. - */ - public Marshaller getMarshaller() { - return this.marshaller; - } - - } - -} +package com.dotmarketing.business.portal; + +import com.dotcms.api.system.event.Payload; +import com.dotcms.api.system.event.SystemEventType; +import com.dotcms.business.CloseDBIfOpened; +import com.dotcms.business.WrapInTransaction; +import com.dotcms.exception.ExceptionUtil; +import com.dotmarketing.business.APILocator; +import com.dotmarketing.business.CacheLocator; +import com.dotmarketing.common.db.DotConnect; +import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.util.Logger; +import com.dotmarketing.util.UtilMethods; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import com.liferay.portal.SystemException; +import com.liferay.portal.ejb.PrincipalBean; +import com.liferay.portal.model.Portlet; +import com.liferay.util.FileUtil; +import io.vavr.control.Try; + +import javax.xml.bind.JAXBException; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +/** + * Implementation class for the {@link PortletFactory} interface. This class uses JAXB to map XML + * files into Java objects. + *

By default, portlet definitions come from three sources: + *

    + *
  • The {@code "/WEB-INF/portlet.xml"} file.
  • + *
  • The {@code "/WEB-INF/portlet-ext.xml"} file.
  • + *
  • The database, where custom Portlets are stored.
  • + *
+ *

+ */ +public class PortletFactoryImpl extends PrincipalBean implements PortletFactory { + + private final String[] systemXmlFiles; + + /** + * Creates an instance of this Factory using the specified XML files as sources of Portlet + * definitions. + * + * @param systemXmlFiles The paths to the XML files to read. + */ + public PortletFactoryImpl(final String[] systemXmlFiles) { + this.systemXmlFiles = systemXmlFiles; + } + + /** + * Default class constructor. It sets the default paths to the system XML files. + */ + public PortletFactoryImpl() { + this(new String[] {FileUtil.getRealPath("/WEB-INF/portlet.xml"), FileUtil.getRealPath("/WEB-INF/portlet-ext.xml")}); + } + + /** + * Reads the contents of the specified XML file, and maps the Portlet definitions from it into + * Portlet objects. + * + * @param pathToXmlFile The path to the XML file to read. + * + * @return A map with the {@link Portlet} definitions. + * + * @throws IOException An error occurred while reading the XML file. + * @throws JAXBException An error occurred while mapping the XML file into Java objects. + */ + private Map xmlToPortlets(final String pathToXmlFile) throws IOException, JAXBException { + if (UtilMethods.isNotSet(pathToXmlFile)) { + return new HashMap<>(); + } + Logger.debug(this,"Loading Portlets from file: " + pathToXmlFile); + try (InputStream stream = new FileInputStream(pathToXmlFile)) { + return xmlToPortlets(stream); + } + } + + /** + * Takes the Input Stream of an XML file and extracts the Portlet definitions from it into + * Portlet objects. + * + * @param fileStream The {@link InputStream} of the XML file to read. + * + * @return A map with the {@link Portlet} definitions. + * + * @throws JAXBException An error occurred while mapping the XML file into Java objects. + */ + private Map xmlToPortlets(final InputStream fileStream) throws IOException { + if (fileStream == null) { + return new HashMap<>(); + } + final Map portlets = new HashMap<>(); + final PortletList portletList = PortletList.builder().fromXml(fileStream); + int counter = 1; + if (UtilMethods.isSet(portletList) && UtilMethods.isSet(portletList.getPortlets())) { + for (final DotPortlet dotPortlets : portletList.getPortlets()) { + portlets.put(dotPortlets.getPortletId(), dotPortlets.toPortlet()); + Logger.debug(this, String.format("%d. Loading portlet ID '%s'", counter, dotPortlets.getPortletId())); + counter++; + } + } + return portlets; + } + + @Override + @VisibleForTesting + public Optional xmlToPortlet(final String xml) throws IOException { + final DotPortlet portlet = DotPortlet.builder().fromXml(xml); + if (portlet.getPortletId() == null || portlet.getPortletClass() == null) { + return Optional.empty(); + } + return Optional.of(portlet.toPortlet()); + } + + @Override + public Map xmlToPortlets(final String[] xmlFiles) throws SystemException { + final Map portlets = new HashMap<>(); + for (final String xmlFile : xmlFiles) { + try { + portlets.putAll(xmlToPortlets(xmlFile)); + } catch (final Exception e) { + throw new SystemException(String.format("An error occurred when loading portlets from XML file " + + "'%s': %s", xmlFile, ExceptionUtil.getErrorMessage(e)), e); + } + } + return portlets; + } + + @Override + public Map xmlToPortlets(final InputStream[] xmlFiles) throws SystemException { + final Map portlets = new HashMap<>(); + for (final InputStream xmlFile : xmlFiles) { + try { + portlets.putAll(xmlToPortlets(xmlFile)); + } catch (final Exception e) { + throw new SystemException(String.format("An error occurred when loading portlets from XML stream: " + + "%s", ExceptionUtil.getErrorMessage(e)), e); + } + } + return portlets; + } + + @WrapInTransaction + @Override + public void deletePortlet(final String portletId) throws DotDataException { + final DotConnect db = new DotConnect(); + db.setSQL("delete from portletpreferences where portletid=?").addParam(portletId).loadResult(); + db.setSQL("delete from portlet where portletid=?").addParam(portletId).loadResult(); + db.setSQL("delete from cms_layouts_portlets where portlet_id=?" ).addParam(portletId).loadResult(); + CacheLocator.getPortletCache().clearCache(); + CacheLocator.getLayoutCache().clearCache(); + APILocator.getSystemEventsAPI().pushAsync(SystemEventType.UPDATE_PORTLET_LAYOUTS, new Payload()); + } + + @Override + public Portlet findById(final String portletId) { + return getPortletMap().get(portletId); + } + + /** + * Loads the dotCMS portlets from both the system XML files and the database. + * + * @return A map with the portlets loaded from the system XML files and the database. + */ + private Map loadSystemPortlets() { + final Map portlets = new HashMap<>(); + + try { + portlets.putAll(xmlToPortlets(this.systemXmlFiles)); + } catch (final Exception e) { + Logger.error(this, e.getMessage(), e); + } + + final List portletList = Try.of(this::findAllDb).getOrElse(Lists.newArrayList()); + for (final Portlet portlet : portletList) { + portlets.put(portlet.getPortletId(), portlet); + } + return portlets; + } + + /** + * Returns the available Portlets in the current dotCMS instance. If the Portlets are not loaded + * in cache yet, it takes care of doing so. + * + * @return A map with the available Portlets. + */ + private Map getPortletMap() { + final PortletCache cache = new PortletCache(); + Map portletsMap = cache.getAllPortlets(); + if (portletsMap.isEmpty()) { + synchronized (PortletCache.class) { + portletsMap = cache.getAllPortlets(); + if (portletsMap.isEmpty()) { + final Map portletMap = this.loadSystemPortlets(); + portletsMap.putAll(portletMap); + cache.putAllPortlets(portletsMap); + } + } + } + return portletsMap; + } + + @Override + public Collection getPortlets() throws SystemException { + return getPortletMap().values(); + } + + /** + * Loads all Portlets directly from the database. Keep in mind that Users can create their own + * portlets for displaying different contents of a given type; e.g., Videos, PDFs, etc. + * + * @return A list with all the Portlets stored in the database. + * + * @throws DotDataException An error occurred while loading the Portlets from the database. + */ + @CloseDBIfOpened + public List findAllDb() throws DotDataException { + final DotConnect db = new DotConnect(); + db.setSQL("select * from portlet where companyid=?").addParam("dotcms.org"); + final List portlets = new ArrayList<>(); + final List> portletsFromDb = db.loadObjectResults(); + for (final Map portletData : portletsFromDb) { + final Portlet testPortlet = new Portlet((String) portletData.get("portletid"), (String) portletData.get("groupid"), (String) portletData.get("companyid"), + (String) portletData.get("defaultpreferences"), false, (String) portletData.get("roles"), true); + + try { + xmlToPortlet((String) portletData.get("defaultpreferences")).ifPresent(portlets::add); + } catch (final Exception e) { + Logger.warn(this.getClass(), String.format("Unable to parse XML code to Portlet with ID '%s': %s", + testPortlet.getPortletId(), ExceptionUtil.getErrorMessage(e))); + } + } + return portlets; + } + + @WrapInTransaction + @Override + public Portlet insertPortlet(final Portlet portlet) throws DotDataException { + if (doesPortletExistInDb(portlet)) { + return updatePortlet(portlet); + } + final String portletXML = portletToXml(portlet); + new DotConnect().setSQL( + "insert into portlet (portletid, groupid, companyid, defaultpreferences, narrow, active_) values(?,?,?,?,?,?)") + .addParam(portlet.getPortletId()) + .addParam(UtilMethods.isSet(portlet.getGroupId())?portlet.getGroupId():"SHARED_KEY") + .addParam(portlet.getCompanyId()) + .addParam(portletXML) + .addParam(portlet.getNarrow()) + .addParam(portlet.getActive()) + .loadResult(); + new PortletCache().clear(); + return portlet; + } + + @WrapInTransaction + @Override + public Portlet updatePortlet(final Portlet portlet) throws DotDataException { + if (!doesPortletExistInDb(portlet)) { + return insertPortlet(portlet); + } + final String portletXML = portletToXml(portlet); + final DotConnect db = new DotConnect(); + db.setSQL("update portlet set groupid=?, defaultpreferences=? where portletid=?") + .addParam(portlet.getGroupId()) + .addParam(portletXML) + .addParam(portlet.getPortletId()) + .loadResult(); + new PortletCache().clear(); + return portlet; + } + + /** + * Checks whether the specified Portlet already exists in the database or not. + * + * @param portlet The {@link Portlet} to check. + * + * @return If the specified Portlet already exists, returns {@code true}. + */ + @CloseDBIfOpened + private boolean doesPortletExistInDb(final Portlet portlet) { + return (new DotConnect().setSQL("select count(*) as test from portlet where portletid=?") + .addParam(portlet.getPortletId()) + .getInt("test") > 0); + } + + @Override + public String portletToXml(final Portlet portlet) throws DotDataException { + try { + return DotPortlet.from(portlet).toXml(); + } catch (final IOException e) { + Logger.error(this, String.format("Failed to transform the Portlet with ID " + + "'%s' into XML: %s", portlet.getPortletId(), ExceptionUtil.getErrorMessage(e))); + throw new DotDataException(e); + } + } + +} \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotmarketing/business/portal/PortletList.java b/dotCMS/src/main/java/com/dotmarketing/business/portal/PortletList.java index dc5fac102f22..762f874bb80f 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/portal/PortletList.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/portal/PortletList.java @@ -1,38 +1,40 @@ package com.dotmarketing.business.portal; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import org.immutables.value.Value; + import java.util.List; +import org.immutables.value.Value.Style.BuilderVisibility; +import org.immutables.value.Value.Style.ImplementationVisibility; -/** - * This class implements the JAXB mapping for the portlet definitions in dotCMS. It takes every - * single portlet definition and maps it to a list of {@link DotPortlet} objects. - * - * @author Jose Castro - * @since Jun 17th, 2024 - */ -@XmlRootElement(name = "portlet-app") -public class PortletList { +@Value.Immutable +@Value.Style( + typeBuilder = "*Builder", + depluralize = true, + visibility = ImplementationVisibility.PRIVATE, + builderVisibility = BuilderVisibility.PUBLIC, + deepImmutablesDetection = true +) +@JacksonXmlRootElement(localName = "portlet-app") +@JsonSerialize(as = PortletList.class) +@JsonDeserialize(builder = PortletList.Builder.class) +public interface PortletList extends XMLSerializable { - private List portlets; + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "portlet") + @JsonProperty("portlet") + List getPortlets(); - /** - * Returns the list of portlets from a given XML file or Input Stream. - * - * @return The list of portlets. - */ - @XmlElement(name = "portlet") - public List getPortlets() { - return portlets; + class Builder extends PortletListBuilder implements XMLEnabledBuilder { } - /** - * Sets the list of portlets. - * - * @param portlets The list of portlets. - */ - public void setPortlets(final List portlets) { - this.portlets = portlets; + static Builder builder() { + return new Builder(); } - -} +} \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotmarketing/business/portal/SerializationHelper.java b/dotCMS/src/main/java/com/dotmarketing/business/portal/SerializationHelper.java new file mode 100644 index 000000000000..01e420d78434 --- /dev/null +++ b/dotCMS/src/main/java/com/dotmarketing/business/portal/SerializationHelper.java @@ -0,0 +1,75 @@ +package com.dotmarketing.business.portal; + + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.datatype.guava.GuavaModule; +import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule; +import java.io.InputStream; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.IOException; + +/* Eventually we may want to reuse this logic generically + at the moment we will encapsulate in this package to keep modular + */ +final class SerializationHelper { + private static final ObjectMapper jsonMapper; + private static final XmlMapper xmlMapper; + + static { + jsonMapper = JsonMapper.builder() + .addModule(new GuavaModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .build(); + + xmlMapper = XmlMapper.builder() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) + .addModule(new JacksonXmlModule()) + .addModule(new JaxbAnnotationModule()) + .addModule(new GuavaModule()) + .defaultUseWrapper(true) + .build(); + } + + private SerializationHelper() { + // Utility class, no instantiation. + } + + public static XmlMapper getXmlMapper() { + return xmlMapper; + } + public static ObjectMapper getJsonMapper() { + return jsonMapper; + } + + public static T fromJson(Class clazz, String json) throws IOException { + return jsonMapper.readValue(json, clazz); + } + + public static T fromXml(Class clazz, String xml) throws IOException { + return xmlMapper.readValue(new StringReader(xml), clazz); + } + + public static T fromJson(Class clazz, InputStream json) throws IOException { + return jsonMapper.readValue(json, clazz); + } + + public static T fromXml(Class clazz, InputStream xml) throws IOException { + return xmlMapper.readValue(xml, clazz); + } + + public static String toJson(Object obj) throws IOException { + return jsonMapper.writeValueAsString(obj); + } + + public static String toXml(Object obj) throws IOException { + StringWriter writer = new StringWriter(); + xmlMapper.writeValue(writer, obj); + return writer.toString(); + } +} \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotmarketing/business/portal/XMLEnabledBuilder.java b/dotCMS/src/main/java/com/dotmarketing/business/portal/XMLEnabledBuilder.java new file mode 100644 index 000000000000..236d0373d021 --- /dev/null +++ b/dotCMS/src/main/java/com/dotmarketing/business/portal/XMLEnabledBuilder.java @@ -0,0 +1,46 @@ +package com.dotmarketing.business.portal; + +import com.dotmarketing.exception.DotRuntimeException; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +public interface XMLEnabledBuilder { + + @JsonIgnore + default Class getInstanceClass() { + // Check the superclass first + Type superclass = getClass().getGenericSuperclass(); + if (superclass instanceof ParameterizedType) { + Type type = ((ParameterizedType) superclass).getActualTypeArguments()[0]; + return (Class) type; + } + + // If not found, check the interfaces specifically for XmlImmutableBuilder + Type[] interfaces = getClass().getGenericInterfaces(); + for (Type iface : interfaces) { + if (iface instanceof ParameterizedType) { + ParameterizedType paramType = (ParameterizedType) iface; + if (paramType.getRawType() == XMLEnabledBuilder.class) { + Type type = paramType.getActualTypeArguments()[0]; + return (Class) type; + } + } + } + + throw new DotRuntimeException("Cannot find parameterized type for XMLEnabledBuilder"); + } + + + + default T fromXml(String xml) throws IOException { + return SerializationHelper.fromXml(getInstanceClass(),xml); + } + + default T fromXml(InputStream is) throws IOException { + return SerializationHelper.fromXml(getInstanceClass(),is); + } + +} diff --git a/dotCMS/src/main/java/com/dotmarketing/business/portal/XMLSerializable.java b/dotCMS/src/main/java/com/dotmarketing/business/portal/XMLSerializable.java new file mode 100644 index 000000000000..447be2430b0d --- /dev/null +++ b/dotCMS/src/main/java/com/dotmarketing/business/portal/XMLSerializable.java @@ -0,0 +1,12 @@ +package com.dotmarketing.business.portal; + +import java.io.IOException; + +interface XMLSerializable { + + + default String toXml() throws IOException { + return SerializationHelper.toXml(this); + } + +} diff --git a/dotCMS/src/main/java/com/dotmarketing/osgi/GenericBundleActivator.java b/dotCMS/src/main/java/com/dotmarketing/osgi/GenericBundleActivator.java index 0a455f9523a9..b358c17b571a 100644 --- a/dotCMS/src/main/java/com/dotmarketing/osgi/GenericBundleActivator.java +++ b/dotCMS/src/main/java/com/dotmarketing/osgi/GenericBundleActivator.java @@ -9,6 +9,10 @@ import static com.dotmarketing.osgi.ActivatorUtil.moveVelocityResources; import static com.dotmarketing.osgi.ActivatorUtil.unfreeze; import static com.dotmarketing.osgi.ActivatorUtil.unregisterAll; + +import com.dotcms.rendering.velocity.util.VelocityUtil; +import com.dotmarketing.business.portal.DotPortlet; +import com.dotmarketing.exception.DotRuntimeException; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; @@ -16,6 +20,8 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; @@ -23,12 +29,11 @@ import java.util.HashSet; import java.util.Iterator; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.apache.felix.framework.OSGIUtil; -import org.apache.felix.http.proxy.DispatcherTracker; import org.apache.velocity.tools.view.PrimitiveToolboxManager; import org.apache.velocity.tools.view.ToolInfo; import org.apache.velocity.tools.view.servlet.ServletToolboxManager; -import org.osgi.framework.Bundle; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; @@ -61,8 +66,6 @@ import com.dotmarketing.servlets.InitServlet; import com.dotmarketing.util.Logger; import com.dotmarketing.util.UtilMethods; -import com.dotmarketing.util.VelocityUtil; -import com.dotmarketing.util.WebKeys; import com.liferay.portal.ejb.PortletManagerUtil; import com.liferay.portal.model.Portlet; import com.liferay.util.Http; @@ -81,6 +84,9 @@ public abstract class GenericBundleActivator implements BundleActivator { private static final String INIT_PARAM_VIEW_JSP = "view-jsp"; private static final String INIT_PARAM_VIEW_TEMPLATE = "view-template"; + public static final String BYTEBUDDY_CLASS_RELOADING_STRATEGY_NOT_SET_JAVA_AGENT_NOT_SET = "bytebuddy ClassReloadingStrategy not set [java agent not set?]"; + public static final String COM_LIFERAY_PORTLET_JSPPORTLET = "com.liferay.portlet.JSPPortlet"; + public static final String COM_LIFERAY_PORTLET_VELOCITY_PORTLET = "com.liferay.portlet.VelocityPortlet"; private BundleContext context; @@ -89,18 +95,18 @@ public abstract class GenericBundleActivator implements BundleActivator { private CacheOSGIService cacheOSGIService; private ConditionletOSGIService conditionletOSGIService; private RuleActionletOSGIService actionletOSGIService; - final private Collection viewTools = new ArrayList<>();; - final private Collection actionlets = new ArrayList<>(); - final private Collection conditionlets = new ArrayList<>(); - final private Collection ruleActionlets = new ArrayList<>(); - final private Collection> cacheProviders = new ArrayList<>(); - final private Map jobs = new HashMap<>(); - final private Collection actions = new ArrayList<>(); - final private Collection portlets = new ArrayList<>(); - final private Collection rules = new ArrayList<>(); - final private Collection preHooks = new ArrayList<>();; - final private Collection postHooks = new ArrayList<>(); - final private Collection overriddenClasses = new HashSet<>(); + private final Collection viewTools = new ArrayList<>(); + private final Collection actionlets = new ArrayList<>(); + private final Collection> conditionlets = new ArrayList<>(); + private final Collection> ruleActionlets = new ArrayList<>(); + private final Collection> cacheProviders = new ArrayList<>(); + private final Map jobs = new HashMap<>(); + private final Collection actions = new ArrayList<>(); + private final Map portlets = new ConcurrentHashMap<>(); + private final Collection rules = new ArrayList<>(); + private final Collection preHooks = new ArrayList<>(); + private final Collection postHooks = new ArrayList<>(); + private final Collection overriddenClasses = new HashSet<>(); protected ClassLoader getBundleClassloader () { return this.getClass().getClassLoader(); @@ -109,7 +115,7 @@ protected ClassLoader getBundleClassloader () { protected ClassLoader getWebAppClassloader () { return InitServlet.class.getClassLoader(); } - + protected ClassReloadingStrategy getClassReloadingStrategy () { try { return ClassReloadingStrategy.fromInstalledAgent(); @@ -126,7 +132,7 @@ protected ClassReloadingStrategy getClassReloadingStrategy () { * * @param context */ - protected void initializeServices ( BundleContext context ) throws Exception { + protected void initializeServices ( BundleContext context ) throws ClassNotFoundException { this.context = context; @@ -150,7 +156,7 @@ protected void initializeServices ( BundleContext context ) throws Exception { *
* New classes will be injected, existing ones will be overridden */ - protected void overrideClasses(final BundleContext context) throws Exception { + protected void overrideClasses(final BundleContext context) throws ClassNotFoundException { if (null == this.context) { this.context = context; @@ -161,11 +167,9 @@ protected void overrideClasses(final BundleContext context) throws Exception { if ( overrideClasses != null && !overrideClasses.isEmpty() ) { String[] forceOverride = overrideClasses.split( "," ); - if ( forceOverride.length > 0 ) { - for ( String classToOverride : forceOverride ) { - //Injecting this bundle context code inside the dotCMS context - overrideClass( classToOverride ); - } + for ( String classToOverride : forceOverride ) { + //Injecting this bundle context code inside the dotCMS context + overrideClass( classToOverride ); } } @@ -176,7 +180,7 @@ protected void overrideClasses(final BundleContext context) throws Exception { * @deprecated Use {@link #overrideClasses(BundleContext)} instead */ @Deprecated - protected void publishBundleServices ( BundleContext context ) throws Exception { + protected void publishBundleServices ( BundleContext context ) throws ClassNotFoundException { overrideClasses(context); } @@ -186,18 +190,15 @@ protected void publishBundleServices ( BundleContext context ) throws Exception * * @param context */ - private void forceToolBoxLoading ( BundleContext context ) { - - ServiceReference serviceRefSelected = context.getServiceReference( PrimitiveToolboxManager.class.getName() ); - if ( serviceRefSelected == null ) { - - //Forcing the loading of the ToolboxManager - ServletToolboxManager toolboxManager = (ServletToolboxManager) VelocityUtil.getToolboxManager(); - if ( toolboxManager != null ) { - - serviceRefSelected = context.getServiceReference( PrimitiveToolboxManager.class.getName() ); - if ( serviceRefSelected == null ) { - toolboxManager.registerService(); + private void forceToolBoxLoading (BundleContext context) { + ServiceReference serviceRefSelected = context.getServiceReference(PrimitiveToolboxManager.class.getName()); + if (serviceRefSelected == null) { + // Forcing the loading of the ToolboxManager + ServletToolboxManager toolboxManagerLocal = (ServletToolboxManager) VelocityUtil.getToolboxManager(); + if (toolboxManagerLocal != null) { + serviceRefSelected = context.getServiceReference(PrimitiveToolboxManager.class.getName()); + if (serviceRefSelected == null) { + toolboxManagerLocal.registerService(); } } } @@ -212,7 +213,7 @@ private void forceToolBoxLoading ( BundleContext context ) { private void forceWorkflowServiceLoading ( BundleContext context ) { //Getting the service to register our Actionlet - ServiceReference serviceRefSelected = context.getServiceReference( WorkflowAPIOsgiService.class.getName() ); + ServiceReference serviceRefSelected = context.getServiceReference( WorkflowAPIOsgiService.class.getName() ); if ( serviceRefSelected == null ) { //Forcing the loading of the WorkflowService @@ -237,7 +238,7 @@ private void forceWorkflowServiceLoading ( BundleContext context ) { private void forceRuleConditionletServiceLoading ( BundleContext context ) { //Getting the service to register our Actionlet. - ServiceReference serviceRefSelected = context.getServiceReference( ConditionletOSGIService.class.getName() ); + ServiceReference serviceRefSelected = context.getServiceReference( ConditionletOSGIService.class.getName() ); if ( serviceRefSelected == null ) { //Forcing the loading of the Rule Conditionlet Service. @@ -262,7 +263,7 @@ private void forceRuleConditionletServiceLoading ( BundleContext context ) { private void forceCacheProviderServiceLoading ( BundleContext context ) { //Getting the service to register our CacheProvider - ServiceReference serviceRefSelected = context.getServiceReference(CacheOSGIService.class.getName()); + ServiceReference serviceRefSelected = context.getServiceReference(CacheOSGIService.class.getName()); if ( serviceRefSelected == null ) { //Forcing the loading of the CacheOSGIService @@ -281,50 +282,49 @@ private void forceCacheProviderServiceLoading ( BundleContext context ) { - - - protected void overrideClass(String className) throws Exception { + + + protected void overrideClass(String className) throws ClassNotFoundException { if (null == getClassReloadingStrategy()) { - Logger.error(this, "bytebuddy ClassReloadingStrategy not set [java agent not set?]"); + Logger.error(this, BYTEBUDDY_CLASS_RELOADING_STRATEGY_NOT_SET_JAVA_AGENT_NOT_SET); return; } className = className.trim(); // Search for the class we want to inject using the felix plugin class loader - Class clazz = Class.forName(className.trim(), false, getBundleClassloader()); + Class clazz = Class.forName(className.trim(), false, getBundleClassloader()); overrideClass(clazz); } - - - - - + + + + + /** * Will inject this bundle context code inside the dotCMS context * - * @param className a reference class inside this bundle jar - * @throws Exception + * @param clazz a reference class inside this bundle jar */ - protected void overrideClass ( Class clazz) throws Exception { + protected void overrideClass ( Class clazz) { if (null == getClassReloadingStrategy()) { - Logger.error(this, "bytebuddy ClassReloadingStrategy not set [java agent not set?]"); + Logger.error(this, BYTEBUDDY_CLASS_RELOADING_STRATEGY_NOT_SET_JAVA_AGENT_NOT_SET); return; } if(!clazz.getClassLoader().equals(getBundleClassloader())) { Logger.error(this, "Class:" + clazz.getName() + " not loaded from bundle classloader, cannot override/inject into dotCMS"); - + } Logger.info(this.getClass().getName(), "Injecting: " + clazz.getName() + " into classloader: " + getWebAppClassloader()); Logger.debug(this.getClass().getName(),"bundle classloader :" +getBundleClassloader() ); Logger.debug(this.getClass().getName(),"context classloader:" +getWebAppClassloader() ); - + ByteBuddyAgent.install(); new ByteBuddy() .rebase(clazz, ClassFileLocator.ForClassLoader.of(getBundleClassloader())) @@ -341,61 +341,74 @@ protected void overrideClass ( Class clazz) throws Exception { //******************************************************************* //******************************************************************* + /** - * Register the portlets on the given configuration files + * Registers portlets based on the provided XML configurations. * - * @param xmls - * @throws Exception - */ - @SuppressWarnings ("unchecked") - protected Collection registerPortlets ( BundleContext context, String[] xmls ) throws Exception { - - - for(String xml :xmls) { - try(InputStream input = new ByteArrayInputStream(Http.URLtoString(context.getBundle().getResource(xml)).getBytes("UTF-8"))){ - portlets.addAll(PortletManagerUtil.addPortlets(new InputStream[]{input})); - } + * @param context the BundleContext + * @param xmls an array of XML file paths + * @return a collection of registered portlets + * @throws Exception if an error occurs during portlet registration + */ + protected Collection registerPortlets(BundleContext context, String[] xmls) throws Exception { + for (String xml : xmls) { + try (InputStream input = new ByteArrayInputStream(Http.URLtoString(context.getBundle().getResource(xml)).getBytes(StandardCharsets.UTF_8))) { + portlets.putAll(PortletManagerUtil.addPortlets(new InputStream[]{input})); + } } - for ( Portlet portlet : portlets ) { - if ( portlet.getPortletClass().equals( "com.liferay.portlet.JSPPortlet" ) ) { + for (Map.Entry entry : portlets.entrySet()) { + Portlet portlet = entry.getValue(); + String portletClass = portlet.getPortletClass(); - Map initParams = portlet.getInitParams(); - String jspPath = (String) initParams.get( INIT_PARAM_VIEW_JSP ); + if (portletClass.equals(COM_LIFERAY_PORTLET_JSPPORTLET) || portletClass.equals( + COM_LIFERAY_PORTLET_VELOCITY_PORTLET)) { + handlePortlet(context, portlet, portletClass); + } - if ( !jspPath.startsWith( PATH_SEPARATOR ) ) { - jspPath = PATH_SEPARATOR + jspPath; - } + Logger.info(this, "Added Portlet: " + portlet.getPortletId()); + OSGIUtil.getInstance().portletIDsStopped.remove(portlet.getPortletId()); + } - //Copy all the resources inside the folder of the given resource to the corresponding dotCMS folders - moveResources( context, jspPath ); - portlet.getInitParams().put( INIT_PARAM_VIEW_JSP, getBundleFolder( context, File.separator ) + jspPath ); - APILocator.getPortletAPI().updatePortlet(portlet); - } else if ( portlet.getPortletClass().equals( "com.liferay.portlet.VelocityPortlet" ) ) { + // Forcing a refresh of the portlets cache + APILocator.getPortletAPI().findAllPortlets(); - Map initParams = portlet.getInitParams(); - String templatePath = (String) initParams.get( INIT_PARAM_VIEW_TEMPLATE ); + return portlets.values(); + } - if ( !templatePath.startsWith( PATH_SEPARATOR ) ) { - templatePath = PATH_SEPARATOR + templatePath; - } + /** + * Handles the processing and updating of a specific portlet type. + * + * @param context the BundleContext + * @param portlet the Portlet object + * @param portletClass the class type of the portlet + * @throws Exception if an error occurs during portlet processing + */ + private void handlePortlet(BundleContext context, Portlet portlet, String portletClass) throws Exception { + Map initParams = portlet.getInitParams(); + String pathParam = portletClass.equals(COM_LIFERAY_PORTLET_JSPPORTLET) ? INIT_PARAM_VIEW_JSP : INIT_PARAM_VIEW_TEMPLATE; + String path = initParams.get(pathParam); - //Copy all the resources inside the folder of the given resource to the corresponding velocity dotCMS folders - moveVelocityResources( context, templatePath ); - portlet.getInitParams().put( INIT_PARAM_VIEW_TEMPLATE, getBundleFolder( context, File.separator ) + templatePath ); - APILocator.getPortletAPI().updatePortlet(portlet); - } + if (!path.startsWith(PATH_SEPARATOR)) { + path = PATH_SEPARATOR + path; + } - Logger.info( this, "Added Portlet: " + portlet.getPortletId() ); - if(OSGIUtil.getInstance().portletIDsStopped.contains(portlet.getPortletId())){ - OSGIUtil.getInstance().portletIDsStopped.remove(portlet.getPortletId()); - } + if (portletClass.equals(COM_LIFERAY_PORTLET_JSPPORTLET)) { + moveResources(context, path); + } else if (portletClass.equals(COM_LIFERAY_PORTLET_VELOCITY_PORTLET)) { + moveVelocityResources(context, path); } - //Forcing a refresh of the portlets cache - APILocator.getPortletAPI().findAllPortlets(); + Map mutableInitParams = new HashMap<>(portlet.getInitParams()); + mutableInitParams.put(pathParam, getBundleFolder(context, File.separator) + path); - return portlets; + DotPortlet updatedDotPortlet = DotPortlet.builder() + .from(portlet) + .initParams(mutableInitParams) + .build(); + + Portlet updatedPortlet = updatedDotPortlet.toPortlet(); + portlets.put(updatedPortlet.getPortletId(), APILocator.getPortletAPI().updatePortlet(updatedPortlet)); } /** @@ -436,7 +449,8 @@ protected ForwardConfig registerActionForward ( BundleContext context, ActionMap * @param actionMapping * @throws Exception */ - protected void registerActionMapping ( ActionMapping actionMapping ) throws Exception { + protected void registerActionMapping ( ActionMapping actionMapping ) + throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { String actionClassType = actionMapping.getType(); @@ -449,7 +463,6 @@ protected void registerActionMapping ( ActionMapping actionMapping ) throws Exce //Adding the ActionConfig to the ForwardConfig moduleConfig.addActionConfig( actionMapping ); - //moduleConfig.freeze(); actions.add( actionMapping ); Logger.info( this, "Added Struts Action Mapping: " + actionClassType ); @@ -461,7 +474,7 @@ protected void registerActionMapping ( ActionMapping actionMapping ) throws Exce * @param scheduledTask * @throws Exception */ - protected void scheduleQuartzJob ( ScheduledTask scheduledTask ) throws Exception { + protected void scheduleQuartzJob ( ScheduledTask scheduledTask ) throws ClassNotFoundException, SchedulerException, ParseException { String jobName = scheduledTask.getJobName(); String jobGroup = scheduledTask.getJobGroup(); @@ -495,7 +508,7 @@ protected void addRewriteRule ( Rule rule ) throws Exception { urlRewriteFilter.addRule( rule ); rules.add( rule ); } else { - throw new RuntimeException( "Non UrlRewriteFilter found!" ); + throw new DotRuntimeException( "Non UrlRewriteFilter found!" ); } } @@ -546,11 +559,10 @@ protected void addRewriteRule ( String from, String to, String type, String name * @param context * @param actionlet */ - @SuppressWarnings ("unchecked") protected void registerActionlet ( BundleContext context, WorkFlowActionlet actionlet ) { //Getting the service to register our Actionlet - ServiceReference serviceRefSelected = context.getServiceReference( WorkflowAPIOsgiService.class.getName() ); + ServiceReference serviceRefSelected = context.getServiceReference( WorkflowAPIOsgiService.class.getName() ); if ( serviceRefSelected == null ) { return; } @@ -561,19 +573,16 @@ protected void registerActionlet ( BundleContext context, WorkFlowActionlet acti actionlets.add( actionlet ); Logger.info( this, "Added actionlet: " + actionlet.getName() ); - if(OSGIUtil.getInstance().actionletsStopped.contains(actionlet.getClass().getCanonicalName())){ - OSGIUtil.getInstance().actionletsStopped.remove(actionlet.getClass().getCanonicalName()); - } + OSGIUtil.getInstance().actionletsStopped.remove(actionlet.getClass().getCanonicalName()); } /** * Register a Rules Engine RuleActionlet service */ - @SuppressWarnings("unchecked") - protected void registerRuleActionlet(BundleContext context, RuleActionlet actionlet) { + protected void registerRuleActionlet(BundleContext context, RuleActionlet actionlet) { //Getting the service to register our Actionlet - ServiceReference serviceRefSelected = context.getServiceReference(RuleActionletOSGIService.class.getName()); + ServiceReference serviceRefSelected = context.getServiceReference(RuleActionletOSGIService.class.getName()); if(serviceRefSelected == null) { return; } @@ -591,11 +600,10 @@ protected void registerRuleActionlet(BundleContext context, RuleActionlet action * @param context * @param conditionlet */ - @SuppressWarnings ("unchecked") - protected void registerRuleConditionlet ( BundleContext context, Conditionlet conditionlet) { + protected void registerRuleConditionlet ( BundleContext context, Conditionlet conditionlet) { //Getting the service to register our Conditionlet - ServiceReference serviceRefSelected = context.getServiceReference( ConditionletOSGIService.class.getName() ); + ServiceReference serviceRefSelected = context.getServiceReference( ConditionletOSGIService.class.getName() ); if ( serviceRefSelected == null ) { return; } @@ -610,63 +618,71 @@ protected void registerRuleConditionlet ( BundleContext context, Conditionlet co } private void registerBundleResourceMessages(BundleContext context) { - //Register Language under /resources/messages folder. Enumeration langFiles = context.getBundle().getEntryPaths("messages"); - while(langFiles != null && langFiles.hasMoreElements() ){ - + while (langFiles != null && langFiles.hasMoreElements()) { String langFile = langFiles.nextElement(); + if (isValidLanguageFile(langFile)) { + processLanguageFile(context, langFile); + } + } + } - //We need to verify file is a language file. - String languageFilePrefix = "messages/Language_"; - String languageFileSuffix = ".properties"; - String languageFileDelimiter = "-"; + private boolean isValidLanguageFile(String langFile) { + String languageFilePrefix = "messages/Language_"; + String languageFileSuffix = ".properties"; + String languageFileDelimiter = "-"; - //Validating Language file name: For example: Language_en-US.properties - if(langFile.startsWith(languageFilePrefix) + return langFile.startsWith(languageFilePrefix) && langFile.contains(languageFileDelimiter) - && langFile.endsWith(languageFileSuffix)){ - - //Get the Language and Country Codes. - String languageCountry = langFile.replace(languageFilePrefix,"").replace(languageFileSuffix, ""); - String languageCode = languageCountry.split(languageFileDelimiter)[0]; - String countryCode = languageCountry.split(languageFileDelimiter)[1]; - - URL file = context.getBundle().getEntry(langFile); - Map generalKeysToAdd = new HashMap<>(); - - try(BufferedReader in = new BufferedReader(new InputStreamReader(file.openStream()))) { - - String line; - while ((line = in.readLine()) != null){ - //Make sure line contains = sign. - String delimiter = "="; - if(line.contains(delimiter)){ - String[] keyValue = line.split(delimiter); - String key = keyValue[0]; - String value = keyValue[1]; - - generalKeysToAdd.put(key,value); - } - } - } catch (IOException e) { - Logger.error(this.getClass(), "Error opening Language File: " + langFile, e); - } + && langFile.endsWith(languageFileSuffix); + } - try { - Language languageObject = APILocator.getLanguageAPI().getLanguage(languageCode, countryCode); - if (null != languageObject && UtilMethods - .isSet(languageObject.getLanguageCode())) { - - APILocator.getLanguageAPI().saveLanguageKeys(languageObject, - generalKeysToAdd, new HashMap<>(), new HashSet<>()); - } else { - Logger.warn(this.getClass(), "Country and Language do not exist: " + languageCountry); - } - - } catch (DotDataException e) { - Logger.error(this.getClass(), "Error inserting language properties for: " + languageCountry, e); + private void processLanguageFile(BundleContext context, String langFile) { + String languageFilePrefix = "messages/Language_"; + String languageFileSuffix = ".properties"; + String languageFileDelimiter = "-"; + + String languageCountry = langFile.replace(languageFilePrefix, "").replace(languageFileSuffix, ""); + String[] languageCountryParts = languageCountry.split(languageFileDelimiter); + String languageCode = languageCountryParts[0]; + String countryCode = languageCountryParts[1]; + + URL file = context.getBundle().getEntry(langFile); + Map generalKeysToAdd = readLanguageFile(file); + + if (generalKeysToAdd != null) { + saveLanguageKeys(languageCode, countryCode, generalKeysToAdd, languageCountry); + } + } + + private Map readLanguageFile(URL file) { + Map generalKeysToAdd = new HashMap<>(); + try (BufferedReader in = new BufferedReader(new InputStreamReader(file.openStream()))) { + String line; + while ((line = in.readLine()) != null) { + if (line.contains("=")) { + String[] keyValue = line.split("=", 2); + generalKeysToAdd.put(keyValue[0], keyValue[1]); } } + } catch (IOException e) { + Logger.error(this.getClass(), "Error opening Language File: " + file, e); + return Map.of(); + } + return generalKeysToAdd; + } + + private void saveLanguageKeys(String languageCode, String countryCode, Map generalKeysToAdd, String languageCountry) { + try { + Language languageObject = APILocator.getLanguageAPI().getLanguage(languageCode, countryCode); + if (languageObject != null && UtilMethods.isSet(languageObject.getLanguageCode())) { + //noinspection removal + APILocator.getLanguageAPI().saveLanguageKeys(languageObject, generalKeysToAdd, new HashMap<>(), new HashSet<>()); + } else { + Logger.warn(this.getClass(), "Country and Language do not exist: " + languageCountry); + } + } catch (DotDataException e) { + Logger.error(this.getClass(), "Error inserting language properties for: " + languageCountry, e); } } @@ -680,11 +696,10 @@ private void registerBundleResourceMessages(BundleContext context) { * @throws InstantiationException * @throws IllegalAccessException */ - @SuppressWarnings ( "unchecked" ) protected void registerCacheProvider ( BundleContext context, String cacheRegion, Class provider ) throws Exception { //Getting the service to register our Cache provider implementation - ServiceReference serviceRefSelected = context.getServiceReference(CacheOSGIService.class.getName()); + ServiceReference serviceRefSelected = context.getServiceReference(CacheOSGIService.class.getName()); if ( serviceRefSelected == null ) { return; } @@ -702,11 +717,10 @@ protected void registerCacheProvider ( BundleContext context, String cacheRegion * @param context * @param info */ - @SuppressWarnings ("unchecked") protected void registerViewToolService ( BundleContext context, ToolInfo info ) { //Getting the service to register our ViewTool - ServiceReference serviceRefSelected = context.getServiceReference( PrimitiveToolboxManager.class.getName() ); + ServiceReference serviceRefSelected = context.getServiceReference( PrimitiveToolboxManager.class.getName() ); if ( serviceRefSelected == null ) { return; } @@ -724,7 +738,8 @@ protected void registerViewToolService ( BundleContext context, ToolInfo info ) * @param preHook * @throws Exception */ - protected void addPreHook ( Object preHook ) throws Exception { + protected void addPreHook ( Object preHook ) + throws ClassNotFoundException, InstantiationException, IllegalAccessException { Interceptor interceptor = (Interceptor) APILocator.getContentletAPIntercepter(); //First we need to be sure we are not adding the same hook more than once @@ -740,7 +755,8 @@ protected void addPreHook ( Object preHook ) throws Exception { * @param postHook * @throws Exception */ - protected void addPostHook ( Object postHook ) throws Exception { + protected void addPostHook ( Object postHook ) + throws ClassNotFoundException, InstantiationException, IllegalAccessException { Interceptor interceptor = (Interceptor) APILocator.getContentletAPIntercepter(); //First we need to be sure we are not adding the same hook more than once @@ -782,13 +798,13 @@ protected void unregisterServices ( BundleContext context ) throws Exception { /** * Unpublish this bundle elements */ - protected void unpublishBundleServices () throws Exception { + protected void unpublishBundleServices () { - if (null != this.overriddenClasses && !this.overriddenClasses.isEmpty()) { + if (!this.overriddenClasses.isEmpty()) { if (null == this.getClassReloadingStrategy()) { Logger.error(this, - "bytebuddy ClassReloadingStrategy not set [java agent not set?]"); + BYTEBUDDY_CLASS_RELOADING_STRATEGY_NOT_SET_JAVA_AGENT_NOT_SET); return; } @@ -811,7 +827,7 @@ protected void unpublishBundleServices () throws Exception { */ protected void unregisterActionlets () { - if ( OSGIUtil.getInstance().workflowOsgiService != null && actionlets != null ) { + if (OSGIUtil.getInstance().workflowOsgiService != null) { for ( WorkFlowActionlet actionlet : actionlets ) { if(!OSGIUtil.getInstance().actionletsStopped.contains(actionlet.getClass().getCanonicalName())){ OSGIUtil.getInstance().actionletsStopped.add(actionlet.getClass().getCanonicalName()); @@ -825,8 +841,8 @@ protected void unregisterActionlets () { */ protected void unregisterConditionlets() { - if (this.conditionletOSGIService != null && conditionlets != null) { - for ( Conditionlet conditionlet : conditionlets ) { + if (this.conditionletOSGIService != null) { + for ( Conditionlet conditionlet : conditionlets ) { this.conditionletOSGIService.removeConditionlet(conditionlet.getClass().getSimpleName()); Logger.info( this, "Removed Rules Conditionlet: " + conditionlet.getClass().getSimpleName()); @@ -839,8 +855,8 @@ protected void unregisterConditionlets() { */ protected void unregisterRuleActionlets() { - if (this.actionletOSGIService != null && ruleActionlets != null) { - for ( RuleActionlet actionlet : ruleActionlets ) { + if (this.actionletOSGIService != null) { + for ( RuleActionlet actionlet : ruleActionlets ) { this.actionletOSGIService.removeRuleActionlet(actionlet.getClass().getSimpleName()); Logger.info( this, "Removed Rules Actionlet: " + actionlet.getClass().getSimpleName()); @@ -853,7 +869,7 @@ protected void unregisterRuleActionlets() { */ protected void unregisterCacheProviders () { - if ( this.cacheOSGIService != null && cacheProviders != null ) { + if (this.cacheOSGIService != null) { for ( Class provider : cacheProviders ) { this.cacheOSGIService.removeCacheProvider(provider); @@ -867,7 +883,7 @@ protected void unregisterCacheProviders () { */ protected void unregisterViewToolServices () { - if ( this.toolboxManager != null && viewTools != null ) { + if (this.toolboxManager != null) { Iterator toolInfoIterator = viewTools.iterator(); while ( toolInfoIterator.hasNext() ) { @@ -887,13 +903,9 @@ protected void unregisterViewToolServices () { * @throws Exception */ protected void unregisterPostHooks () { - - if ( postHooks != null ) { - - Interceptor interceptor = (Interceptor) APILocator.getContentletAPIntercepter(); - for ( String postHook : postHooks ) { - interceptor.delPostHookByClassName( postHook ); - } + Interceptor interceptor = (Interceptor) APILocator.getContentletAPIntercepter(); + for ( String postHook : postHooks ) { + interceptor.delPostHookByClassName( postHook ); } } @@ -904,12 +916,9 @@ protected void unregisterPostHooks () { */ protected void unregisterPreHooks () { - if ( preHooks != null ) { - - Interceptor interceptor = (Interceptor) APILocator.getContentletAPIntercepter(); - for ( String preHook : preHooks ) { - interceptor.delPreHookByClassName( preHook ); - } + Interceptor interceptor = (Interceptor) APILocator.getContentletAPIntercepter(); + for ( String preHook : preHooks ) { + interceptor.delPreHookByClassName( preHook ); } } @@ -918,19 +927,16 @@ protected void unregisterPreHooks () { * * @throws Exception */ - protected void unregisterActionMappings () throws Exception { - - if ( actions != null ) { + protected void unregisterActionMappings () throws NoSuchFieldException, IllegalAccessException { - ModuleConfig moduleConfig = getModuleConfig(); - //We need to unfreeze this module in order to add new action mappings - unfreeze( moduleConfig ); + ModuleConfig moduleConfig = getModuleConfig(); + //We need to unfreeze this module in order to add new action mappings + unfreeze( moduleConfig ); - for ( ActionConfig actionConfig : actions ) { - moduleConfig.removeActionConfig( actionConfig ); - } - moduleConfig.freeze(); + for ( ActionConfig actionConfig : actions ) { + moduleConfig.removeActionConfig( actionConfig ); } + moduleConfig.freeze(); } @@ -939,12 +945,10 @@ protected void unregisterActionMappings () throws Exception { * * @throws SchedulerException */ - protected void unregisterQuartzJobs () throws Exception { + protected void unregisterQuartzJobs () throws SchedulerException { - if ( jobs != null ) { - for ( String jobName : jobs.keySet() ) { - QuartzUtils.removeJob( jobName, jobs.get( jobName ) ); - } + for ( Map.Entry entry : jobs.entrySet() ) { + QuartzUtils.removeJob( entry.getKey(), entry.getValue() ); } } @@ -953,13 +957,10 @@ protected void unregisterQuartzJobs () throws Exception { * * @throws SchedulerException */ - protected void unregisterPortlets () throws Exception { - if ( portlets != null ) { - - for ( Portlet portlet : portlets ) { - if(!OSGIUtil.getInstance().portletIDsStopped.contains(portlet.getPortletId())){ - OSGIUtil.getInstance().portletIDsStopped.add(portlet.getPortletId()); - } + protected void unregisterPortlets () { + for ( Portlet portlet : portlets.values() ) { + if(!OSGIUtil.getInstance().portletIDsStopped.contains(portlet.getPortletId())){ + OSGIUtil.getInstance().portletIDsStopped.add(portlet.getPortletId()); } } } @@ -980,18 +981,15 @@ protected void unregisterServlets ( BundleContext context ) throws Exception { */ protected void unregisterRewriteRule () throws Exception { - if ( rules != null ) { - - //Get a reference of our url rewrite filter - DotUrlRewriteFilter urlRewriteFilter = DotUrlRewriteFilter.getUrlRewriteFilter(); - if ( urlRewriteFilter != null ) { - for ( Rule rule : rules ) { - //Remove from the filter this rule - urlRewriteFilter.removeRule( rule ); - } - } else { - throw new RuntimeException( "Non UrlRewriteFilter found!" ); + //Get a reference of our url rewrite filter + DotUrlRewriteFilter urlRewriteFilter = DotUrlRewriteFilter.getUrlRewriteFilter(); + if ( urlRewriteFilter != null ) { + for ( Rule rule : rules ) { + //Remove from the filter this rule + urlRewriteFilter.removeRule( rule ); } + } else { + throw new DotRuntimeException( "Non UrlRewriteFilter found!" ); } } diff --git a/dotCMS/src/main/java/com/liferay/portal/ejb/PortletManagerUtil.java b/dotCMS/src/main/java/com/liferay/portal/ejb/PortletManagerUtil.java index dcb896698bff..04d386e44b94 100644 --- a/dotCMS/src/main/java/com/liferay/portal/ejb/PortletManagerUtil.java +++ b/dotCMS/src/main/java/com/liferay/portal/ejb/PortletManagerUtil.java @@ -38,18 +38,9 @@ public class PortletManagerUtil { @WrapInTransaction - public static Collection addPortlets(final InputStream[] xmls) throws com.liferay.portal.SystemException { + public static Map addPortlets(final InputStream[] xmls) throws com.liferay.portal.SystemException { try { - final PortletFactory portletFactory = PortletManagerFactory.getManager(); - - Collection portlets = new ArrayList<>(); - - final Map foundPortlets = portletFactory.xmlToPortlets(xmls); - for(Portlet portlet : foundPortlets.values()) { - portlets.add(portletFactory.insertPortlet(portlet)); - } - - return portlets; + return PortletManagerFactory.getManager().xmlToPortlets(xmls); } catch (final Exception e) { throw new com.liferay.portal.SystemException(e); } diff --git a/dotCMS/src/main/java/com/liferay/portal/servlet/MainServlet.java b/dotCMS/src/main/java/com/liferay/portal/servlet/MainServlet.java index b46a786493a0..41352da19e90 100644 --- a/dotCMS/src/main/java/com/liferay/portal/servlet/MainServlet.java +++ b/dotCMS/src/main/java/com/liferay/portal/servlet/MainServlet.java @@ -174,34 +174,6 @@ public void init(ServletConfig config) throws ServletException { throw new DotRuntimeException(e); } - - // Scheduler - - // try { - // Iterator itr = - // PortletManagerUtil.getPortlets(_companyId).iterator(); - // - // while (itr.hasNext()) { - // Portlet portlet = (Portlet)itr.next(); - // - // String className = portlet.getSchedulerClass(); - // - // if (portlet.isActive() && className != null) { - // Scheduler scheduler = - // (Scheduler)InstancePool.get(className); - // - // scheduler.schedule(); - // } - // } - // } - // catch (ObjectAlreadyExistsException oaee) { - // } - // catch (Exception e) { - // Logger.error(this,e.getMessage(),e); - // } - - // Message Resources - MultiMessageResources messageResources = (MultiMessageResources) ctx.getAttribute(Globals.MESSAGES_KEY); messageResources.setServletContext(ctx); diff --git a/dotCMS/src/main/resources/osgi/osgi-extra.conf b/dotCMS/src/main/resources/osgi/osgi-extra.conf index 1bfd09e54d51..332dab7008cc 100644 --- a/dotCMS/src/main/resources/osgi/osgi-extra.conf +++ b/dotCMS/src/main/resources/osgi/osgi-extra.conf @@ -399,6 +399,7 @@ com.dotmarketing.portlets.linkchecker.bean, com.dotcms.enterprise.achecker.validation, org.apache.velocity.texen.util, com.dotcms.concurrent, +com.dotcms.http, com.dotcms.visitor.domain, com.dotmarketing.portlets.linkchecker.util, com.dotmarketing.portlets.workflowmessages.struts, diff --git a/dotCMS/src/test/java/com/dotmarketing/business/portal/DotPortletTest.java b/dotCMS/src/test/java/com/dotmarketing/business/portal/DotPortletTest.java new file mode 100644 index 000000000000..5f5815e6932e --- /dev/null +++ b/dotCMS/src/test/java/com/dotmarketing/business/portal/DotPortletTest.java @@ -0,0 +1,126 @@ +package com.dotmarketing.business.portal; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.liferay.portal.model.Portlet; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; + +public class DotPortletTest { + + private DotPortlet testPortlet; + private ObjectMapper jsonMapper; + private XmlMapper xmlMapper; + + @Before + public void setUp() { + Map initParams = new HashMap<>(); + initParams.put("param1", "value1"); + initParams.put("param2", "value2"); + + testPortlet = DotPortlet.builder() + .portletId("test-portlet") + .portletClass("com.example.TestPortlet") + .initParams(initParams) + .build(); + + jsonMapper = new ObjectMapper(); + xmlMapper = new XmlMapper(); + } + + @Test + public void testGetPortletId() { + assertEquals("test-portlet", testPortlet.getPortletId()); + } + + @Test + public void testGetPortletClass() { + assertEquals("com.example.TestPortlet", testPortlet.getPortletClass()); + } + + @Test + public void testGetInitParams() { + List initParams = testPortlet.getInitParams(); + assertEquals(2, initParams.size()); + assertTrue(initParams.contains(InitParam.of("param1", "value1"))); + assertTrue(initParams.contains(InitParam.of("param2", "value2"))); + } + + @Test + public void testInitParamsMap() { + Map initParamsMap = testPortlet.initParams(); + assertEquals(2, initParamsMap.size()); + assertEquals("value1", initParamsMap.get("param1")); + assertEquals("value2", initParamsMap.get("param2")); + } + + @Test + public void testFromPortlet() { + Portlet portlet = new Portlet("test-portlet", "com.example.TestPortlet", testPortlet.initParams()); + DotPortlet converted = DotPortlet.from(portlet); + assertEquals(testPortlet, converted); + } + + @Test + public void testToPortlet() { + Portlet portlet = testPortlet.toPortlet(); + assertEquals(testPortlet.getPortletId(), portlet.getPortletId()); + assertEquals(testPortlet.getPortletClass(), portlet.getPortletClass()); + assertEquals(testPortlet.initParams(), portlet.getInitParams()); + } + + @Test + public void testJsonSerialization() throws IOException { + String json = jsonMapper.writeValueAsString(testPortlet); + DotPortlet deserialized = jsonMapper.readValue(json, DotPortlet.class); + assertEquals(testPortlet, deserialized); + } + + @Test + public void testXmlSerialization() throws IOException { + String xml = xmlMapper.writeValueAsString(testPortlet); + DotPortlet deserialized = xmlMapper.readValue(xml, DotPortlet.class); + assertEquals(testPortlet, deserialized); + } + + @Test + public void testBuilder() { + DotPortlet.Builder builder = DotPortlet.builder() + .portletId("builder-test") + .portletClass("com.example.BuilderTest"); + + builder.addAllInitParams(List.of( + InitParam.of("param1", "value1"), + InitParam.of("param2", "value2") + )); + + DotPortlet built = builder.build(); + assertEquals("builder-test", built.getPortletId()); + assertEquals("com.example.BuilderTest", built.getPortletClass()); + assertEquals(2, built.getInitParams().size()); + } + + @Test + public void testBuilderFromPortlet() { + Portlet portlet = new Portlet("from-portlet", "com.example.FromPortlet", Map.of("key", "value")); + DotPortlet built = DotPortlet.builder().from(portlet).build(); + assertEquals(portlet.getPortletId(), built.getPortletId()); + assertEquals(portlet.getPortletClass(), built.getPortletClass()); + assertEquals(portlet.getInitParams(), built.initParams()); + } + + @Test + public void testXmlSerializable() throws IOException { + String xml = testPortlet.toXml(); + DotPortlet deserialized = DotPortlet.builder().fromXml(xml); + assertEquals(testPortlet, deserialized); + } +} \ No newline at end of file diff --git a/dotCMS/src/test/java/com/dotmarketing/business/portal/PortletListTest.java b/dotCMS/src/test/java/com/dotmarketing/business/portal/PortletListTest.java new file mode 100644 index 000000000000..c7fc96790970 --- /dev/null +++ b/dotCMS/src/test/java/com/dotmarketing/business/portal/PortletListTest.java @@ -0,0 +1,162 @@ +package com.dotmarketing.business.portal; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; + +public class PortletListTest { + + private PortletList testPortletList; + private ObjectMapper jsonMapper; + private XmlMapper xmlMapper; + + @Before + public void setUp() { + DotPortlet portlet1 = DotPortlet.builder() + .portletId("portlet1") + .portletClass("com.example.Portlet1") + .putInitParam("key1", "value1") + .build(); + + DotPortlet portlet2 = DotPortlet.builder() + .portletId("portlet2") + .portletClass("com.example.Portlet2") + .putInitParam("key2", "value2") + .build(); + + testPortletList = PortletList.builder() + .addPortlets(portlet1, portlet2) + .build(); + + jsonMapper = new ObjectMapper(); + xmlMapper = new XmlMapper(); + } + + @Test + public void testGetPortlets() { + List portlets = testPortletList.getPortlets(); + assertEquals(2, portlets.size()); + assertEquals("portlet1", portlets.get(0).getPortletId()); + assertEquals("portlet2", portlets.get(1).getPortletId()); + } + + @Test + public void testBuilder() { + PortletList.Builder builder = PortletList.builder(); + DotPortlet portlet = DotPortlet.builder() + .portletId("testPortlet") + .portletClass("com.example.TestPortlet") + .build(); + + PortletList built = builder.addPortlets(portlet).build(); + assertEquals(1, built.getPortlets().size()); + assertEquals("testPortlet", built.getPortlets().get(0).getPortletId()); + } + + @Test + public void testBuilderAddAll() { + PortletList.Builder builder = PortletList.builder(); + DotPortlet portlet1 = DotPortlet.builder() + .portletId("portlet1") + .portletClass("com.example.Portlet1") + .build(); + DotPortlet portlet2 = DotPortlet.builder() + .portletId("portlet2") + .portletClass("com.example.Portlet2") + .build(); + + PortletList built = builder.addAllPortlets(Arrays.asList(portlet1, portlet2)).build(); + assertEquals(2, built.getPortlets().size()); + } + + @Test + public void testJsonSerialization() throws IOException { + String json = jsonMapper.writeValueAsString(testPortletList); + PortletList deserialized = jsonMapper.readValue(json, PortletList.class); + assertEquals(testPortletList.getPortlets().size(), deserialized.getPortlets().size()); + assertEquals(testPortletList.getPortlets().get(0).getPortletId(), deserialized.getPortlets().get(0).getPortletId()); + } + + @Test + public void testXmlSerialization() throws IOException { + String xml = xmlMapper.writeValueAsString(testPortletList); + PortletList deserialized = xmlMapper.readValue(xml, PortletList.class); + assertEquals(testPortletList.getPortlets().size(), deserialized.getPortlets().size()); + assertEquals(testPortletList.getPortlets().get(0).getPortletId(), deserialized.getPortlets().get(0).getPortletId()); + } + + @Test + public void testXmlSerializable() throws IOException { + String xml = testPortletList.toXml(); + PortletList deserialized = PortletList.builder().fromXml(xml); + assertEquals(testPortletList.getPortlets().size(), deserialized.getPortlets().size()); + assertEquals(testPortletList.getPortlets().get(0).getPortletId(), deserialized.getPortlets().get(0).getPortletId()); + } + + @Test + public void testEmptyPortletList() { + PortletList emptyList = PortletList.builder().build(); + assertTrue(emptyList.getPortlets().isEmpty()); + } + + @Test + public void testImmutability() { + List portlets = testPortletList.getPortlets(); + DotPortlet builder = DotPortlet.builder() + .portletId("newPortlet") + .portletClass("com.example.NewPortlet") + .build(); + try { + portlets.add(builder); + fail("PortletList should be immutable"); + } catch (UnsupportedOperationException e) { + // Expected exception + } + } + + @Test + public void testBuilderReset() { + PortletList.Builder builder = PortletList.builder() + .addPortlets(DotPortlet.builder() + .portletId("portlet1") + .portletClass("com.example.Portlet1") + .build()); + + builder.portlets(Collections.emptyList()); + PortletList reset = builder.build(); + assertTrue(reset.getPortlets().isEmpty()); + } + + @Test(expected = IllegalStateException.class) + public void testDotPortletMissingPortletClass() { + DotPortlet.builder() + .portletId("testPortlet") + .build(); + } + + @Test(expected = IllegalStateException.class) + public void testDotPortletMissingPortletId() { + DotPortlet.builder() + .portletClass("com.example.TestPortlet") + .build(); + } + + @Test + public void testDotPortletBuilderWithAllRequiredFields() { + DotPortlet portlet = DotPortlet.builder() + .portletId("testPortlet") + .portletClass("com.example.TestPortlet") + .build(); + assertNotNull(portlet); + assertEquals("testPortlet", portlet.getPortletId()); + assertEquals("com.example.TestPortlet", portlet.getPortletClass()); + } +} \ No newline at end of file diff --git a/dotCMS/src/test/java/com/dotmarketing/business/portal/SerializationHelperTest.java b/dotCMS/src/test/java/com/dotmarketing/business/portal/SerializationHelperTest.java new file mode 100644 index 000000000000..4b3acccdaf5a --- /dev/null +++ b/dotCMS/src/test/java/com/dotmarketing/business/portal/SerializationHelperTest.java @@ -0,0 +1,178 @@ +package com.dotmarketing.business.portal; + +import static org.junit.Assert.*; + +import com.dotmarketing.util.Logger; +import java.util.Optional; +import org.junit.Before; +import org.junit.Test; +import java.io.FileInputStream; +import java.io.IOException; + +public class SerializationHelperTest { + + private DotPortlet testPortlet; + private PortletList testPortletList; + + @Before + public void setUp() { + testPortlet = DotPortlet.builder() + .portletId("categories") + .portletClass("com.liferay.portlet.StrutsPortlet") + .putInitParam("view-action", "/ext/c") + .putInitParam("param2", "param2.value") + .build(); + + testPortletList = PortletList.builder() + .addPortlet(testPortlet) + .build(); + } + + @Test + public void testFromXmlFile() throws IOException { + try (FileInputStream fis = new FileInputStream("src/main/webapp/WEB-INF/portlet.xml")) { + PortletList portletList = SerializationHelper.fromXml(PortletList.class, fis); + Logger.debug(SerializationHelperTest.class,"Loaded src/main/webapp/WEB-INF/portlet.xml:"+portletList.toString()); + Logger.info(SerializationHelperTest.class, "Loaded portlet.xml: found: " + portletList.getPortlets().size() + " portlets"); + assertNotNull("Deserialized PortletList should not be null", portletList); + assertEquals("PortletList should contain exactly 44 portlets", 44, portletList.getPortlets().size()); + + // Check for specific portlets + assertTrue("PortletList should contain 'categories' portlet", + portletList.getPortlets().stream().anyMatch(p -> p.getPortletId().equals("categories"))); + assertTrue("PortletList should contain 'dotai' portlet", + portletList.getPortlets().stream().anyMatch(p -> p.getPortletId().equals("dotai"))); + + // Check a specific portlet's details + Optional categoriesPortlet = portletList.getPortlets().stream() + .filter(p -> p.getPortletId().equals("categories")) + .findFirst(); + assertTrue("Categories portlet should exist", categoriesPortlet.isPresent()); + categoriesPortlet.ifPresent(portlet -> { + assertEquals("Categories portlet class should match", + "com.liferay.portlet.StrutsPortlet", portlet.getPortletClass()); + assertEquals("Categories portlet should have correct view-action", + "/ext/categories/view_categories", portlet.initParams().get("view-action")); + }); + } + } + + @Test + public void testPortletJsonSerialization() throws IOException { + String json = SerializationHelper.toJson(testPortlet); + Logger.info(SerializationHelperTest.class,"Portlet to Json: "+json); + assertNotNull("JSON string should not be null", json); + assertTrue("JSON should contain portlet ID", json.contains("\"portlet-name\":\"categories\"")); + assertTrue("JSON should contain portlet class", json.contains("\"portlet-class\":\"com.liferay.portlet.StrutsPortlet\"")); + assertTrue("JSON should contain init-param", json.contains("\"init-param\":[{")); + assertTrue("JSON should contain view-action param", json.contains("\"name\":\"view-action\",\"value\":\"/ext/c\"")); + assertTrue("JSON should contain param2", json.contains("\"name\":\"param2\",\"value\":\"param2.value\"")); + } + + @Test + public void testPortletListJsonSerialization() throws IOException { + String jsonList = SerializationHelper.toJson(testPortletList); + Logger.info(SerializationHelperTest.class,"PortletList to Json: "+jsonList); + assertNotNull("JSON string should not be null", jsonList); + assertTrue("JSON should contain portlet array", jsonList.contains("\"portlet\":[{")); + assertTrue("JSON should contain portlet ID", jsonList.contains("\"portlet-name\":\"categories\"")); + assertTrue("JSON should contain portlet class", jsonList.contains("\"portlet-class\":\"com.liferay.portlet.StrutsPortlet\"")); + assertTrue("JSON should contain init-param", jsonList.contains("\"init-param\":[{")); + } + + @Test + public void testPortletListJsonDeserialization() throws IOException { + String jsonList = SerializationHelper.toJson(testPortletList); + PortletList deserializedJson = SerializationHelper.fromJson(PortletList.class, jsonList); + Logger.info(SerializationHelperTest.class,"PortletList to Json: "+jsonList); + assertNotNull("Deserialized PortletList should not be null", deserializedJson); + assertEquals("PortletList should contain one portlet", 1, deserializedJson.getPortlets().size()); + DotPortlet deserializedPortlet = deserializedJson.getPortlets().get(0); + assertEquals("Portlet ID should match", "categories", deserializedPortlet.getPortletId()); + assertEquals("Portlet class should match", "com.liferay.portlet.StrutsPortlet", deserializedPortlet.getPortletClass()); + assertEquals("Portlet should have 2 init params", 2, deserializedPortlet.initParams().size()); + assertEquals("view-action param should match", "/ext/c", deserializedPortlet.initParams().get("view-action")); + assertEquals("param2 should match", "param2.value", deserializedPortlet.initParams().get("param2")); + } + + @Test + public void testPortletXmlSerialization() throws IOException { + String portletXml = SerializationHelper.toXml(testPortlet); + Logger.info(SerializationHelperTest.class,"Portlet to Xml: "+portletXml); + assertNotNull("XML string should not be null", portletXml); + assertTrue("XML should contain portlet tag", portletXml.contains("")); + assertTrue("XML should contain portlet-name", portletXml.contains("categories")); + assertTrue("XML should contain portlet-class", portletXml.contains("com.liferay.portlet.StrutsPortlet")); + assertTrue("XML should contain init-param", portletXml.contains("")); + assertTrue("XML should contain view-action param", portletXml.contains("view-action/ext/c")); + assertTrue("XML should contain param2", portletXml.contains("param2param2.value")); + } + + @Test + public void testPortletListXmlSerialization() throws IOException { + String xml = SerializationHelper.toXml(testPortletList); + Logger.info(SerializationHelperTest.class,"PortletList to xml: "+xml); + assertNotNull("XML string should not be null", xml); + assertTrue("XML should contain portlet-app tag", xml.contains("")); + assertTrue("XML should contain portlet tag", xml.contains("")); + assertTrue("XML should contain portlet-name", xml.contains("categories")); + assertTrue("XML should contain portlet-class", xml.contains("com.liferay.portlet.StrutsPortlet")); + assertTrue("XML should contain init-param", xml.contains("")); + } + + @Test + public void testPortletXmlDeserialization() throws IOException { + String portletXml = SerializationHelper.toXml(testPortlet); + DotPortlet deserializedPortletXml = SerializationHelper.fromXml(DotPortlet.class, portletXml); + assertNotNull("Deserialized DotPortlet should not be null", deserializedPortletXml); + assertEquals("Portlet ID should match", "categories", deserializedPortletXml.getPortletId()); + assertEquals("Portlet class should match", "com.liferay.portlet.StrutsPortlet", deserializedPortletXml.getPortletClass()); + assertEquals("Portlet should have 2 init params", 2, deserializedPortletXml.initParams().size()); + assertEquals("view-action param should match", "/ext/c", deserializedPortletXml.initParams().get("view-action")); + assertEquals("param2 should match", "param2.value", deserializedPortletXml.initParams().get("param2")); + } + + @Test + public void testPortletListXmlDeserialization() throws IOException { + String xml = SerializationHelper.toXml(testPortletList); + PortletList deserializedXml = SerializationHelper.fromXml(PortletList.class, xml); + assertNotNull("Deserialized PortletList should not be null", deserializedXml); + assertEquals("PortletList should contain one portlet", 1, deserializedXml.getPortlets().size()); + + Optional deserializedPortlet = deserializedXml.getPortlets().stream() + .filter(p -> p.getPortletId().equals("categories")) + .findFirst(); + + assertTrue("Deserialized PortletList should contain 'categories' portlet", deserializedPortlet.isPresent()); + + deserializedPortlet.ifPresent(portlet -> { + assertEquals("Portlet ID should match", "categories", portlet.getPortletId()); + assertEquals("Portlet class should match", "com.liferay.portlet.StrutsPortlet", portlet.getPortletClass()); + assertEquals("Portlet should have 2 init params", 2, portlet.initParams().size()); + assertEquals("view-action param should match", "/ext/c", portlet.initParams().get("view-action")); + assertEquals("param2 should match", "param2.value", portlet.initParams().get("param2")); + }); + } + + @Test + public void testPortletListXmlRoundTrip() throws IOException { + String xml = testPortletList.toXml(); + PortletList newPortletList = PortletList.builder().fromXml(xml); + assertNotNull("New PortletList should not be null", newPortletList); + assertEquals("PortletList should contain one portlet", 1, newPortletList.getPortlets().size()); + + Optional roundTripPortlet = newPortletList.getPortlets().stream() + .filter(p -> p.getPortletId().equals("categories")) + .findFirst(); + + assertTrue("Round-tripped PortletList should contain 'categories' portlet", roundTripPortlet.isPresent()); + + roundTripPortlet.ifPresent(portlet -> { + assertEquals("Portlet ID should match", "categories", portlet.getPortletId()); + assertEquals("Portlet class should match", "com.liferay.portlet.StrutsPortlet", portlet.getPortletClass()); + assertEquals("Portlet should have 2 init params", 2, portlet.initParams().size()); + assertEquals("view-action param should match", "/ext/c", portlet.initParams().get("view-action")); + assertEquals("param2 should match", "param2.value", portlet.initParams().get("param2")); + }); + } +} \ No newline at end of file diff --git a/dotcms-integration/src/test/java/com/dotcms/util/IntegrationTestInitService.java b/dotcms-integration/src/test/java/com/dotcms/util/IntegrationTestInitService.java index db5fed05de31..372b46971542 100644 --- a/dotcms-integration/src/test/java/com/dotcms/util/IntegrationTestInitService.java +++ b/dotcms-integration/src/test/java/com/dotcms/util/IntegrationTestInitService.java @@ -14,11 +14,11 @@ import com.dotmarketing.util.Logger; import com.liferay.util.SystemProperties; +import java.time.Duration; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.awaitility.Awaitility; -import org.awaitility.Duration; import org.mockito.Mockito; /** @@ -50,7 +50,7 @@ public void init() throws Exception { Awaitility.setDefaultPollInterval(10, TimeUnit.MILLISECONDS); Awaitility.setDefaultPollDelay(Duration.ZERO); - Awaitility.setDefaultTimeout(Duration.ONE_MINUTE); + Awaitility.setDefaultTimeout(Duration.ofMinutes(1)); ConfigTestHelper._setupFakeTestingContext(); diff --git a/dotcms-integration/src/test/java/com/dotmarketing/business/portal/PortletAPIImplTest.java b/dotcms-integration/src/test/java/com/dotmarketing/business/portal/PortletAPIImplTest.java index fb3497e65b5d..7c894d3adde5 100644 --- a/dotcms-integration/src/test/java/com/dotmarketing/business/portal/PortletAPIImplTest.java +++ b/dotcms-integration/src/test/java/com/dotmarketing/business/portal/PortletAPIImplTest.java @@ -12,6 +12,7 @@ import com.dotmarketing.business.APILocator; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.util.Config; +import com.dotmarketing.util.Logger; import com.liferay.portal.SystemException; import com.liferay.portal.language.LanguageException; import com.liferay.portal.model.Portlet; @@ -24,9 +25,7 @@ import com.tngtech.java.junit.dataprovider.UseDataProvider; import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; import java.util.List; -import java.util.Map; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -62,17 +61,18 @@ private Portlet createCustomPortlet(final String name, final String portletId, f private Portlet createCustomPortlet(final String name, final String portletId, final String baseTypes, final String contentTypes, final String dataViewMode) throws LanguageException, DotDataException { - final Map initValues = new HashMap<>(); - initValues.put("view-action","/ext/contentlet/view_contentlets"); - initValues.put("name", name); - initValues.put("baseTypes", baseTypes); - initValues.put("contentTypes", contentTypes); - initValues.put("dataViewMode", dataViewMode); + final DotPortlet newPortlet = DotPortlet.builder() + .portletId(portletId) + .portletClass(StrutsPortlet.class.getName()) + .putInitParam("view-action", "/ext/contentlet/view_contentlets") + .putInitParam("name", name) + .putInitParam("baseTypes", baseTypes) + .putInitParam("contentTypes", contentTypes) + .putInitParam("dataViewMode", dataViewMode) + .build(); - final Portlet newPortlet = portletApi.savePortlet(new DotPortlet(portletId, StrutsPortlet.class.getName(), initValues),systemUser); - - return newPortlet; + return portletApi.savePortlet(newPortlet.toPortlet(), systemUser); } /** @@ -107,7 +107,12 @@ public void test_createCustomPortlet(final testCaseCreateCustomPortlet testCase) contentType -> returnedContentTypes.contains(contentType.trim()))); } }catch (DotDataException | IllegalArgumentException e){ - Assert.assertFalse(testCase.createdSuccessfully); + if (testCase.createdSuccessfully) { + Logger.error(PortletAPIImplTest.class, "Did not expect Exception for test", e); + } else { + Logger.info(PortletAPIImplTest.class,"Expected Exception found: " + e.getMessage()); + } + Assert.assertFalse(e.getMessage(),testCase.createdSuccessfully); return; }finally { if(portlet!=null){ diff --git a/dotcms-postman/src/main/resources/postman/EnvironmentResource.postman_collection.json b/dotcms-postman/src/main/resources/postman/EnvironmentResource.postman_collection.json index 2eb5dc0edf6a..2ae21ea37dec 100644 --- a/dotcms-postman/src/main/resources/postman/EnvironmentResource.postman_collection.json +++ b/dotcms-postman/src/main/resources/postman/EnvironmentResource.postman_collection.json @@ -408,13 +408,25 @@ "pm.test(\"Status code should be 400\", function () {", " pm.response.to.have.status(400);", "});", - "", "pm.test(\"Response include proper message\", function () {", - " var response = pm.response.json();", - " pm.expect(response[0].fieldName, 'FAILED:[fieldName]').eql(\"pushMode\");", - " pm.expect(response[0].message, 'FAILED:[message]').eql(\"may not be null\");", - "});", - "" + " var response;", + " // Try to parse the response as JSON", + " try {", + " response = pm.response.json();", + " } catch (e) {", + " pm.expect.fail(\"Response is not in JSON format\");", + " return;", + " }", + " // Log the response for debugging purposes", + " console.log(response);", + " // Check if response is an array and has at least one element", + " pm.expect(Array.isArray(response), 'Response is not an array').to.be.true;", + " pm.expect(response.length, 'Response array is empty').to.be.above(0);", + " // Check if the necessary fields are present in the first element of the response", + " pm.expect(response[0], 'First element of response is undefined').to.be.an('object').that.includes.keys('fieldName', 'message');", + " pm.expect(response[0].fieldName, 'FAILED: [fieldName]').to.eql(\"pushMode\");", + " pm.expect(response[0].message, 'FAILED: [message]').to.eql(\"may not be null\");", + "});" ], "type": "text/javascript", "packages": {}