Uri.java

/*
** Module   : Uri.java
** Abstract : Implementation of the builtin class.
**
** Copyright (c) 2019-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 CA  20190526 First version, stubs taken by converting the skeleton using FWD.
** 002 CA  20190710 Runtime implementation for some APIs.
** 003 CA  20191024 Added method support levels and updated the class support level.
** 004 CA  20200427 Fixed toLegacyString() and a fix for URI parse.
** 005 CA  20200910 Fixed QueryString property - if missing in the URI, must be empty space and not unknown.
**     ME  20200912 Accumulated changes.
** 006 ME  20200925 Rewrite the 4GL way (encode/decode/parse), add missing/new methods as per OE12.2.
**     ME  20210111 Only add fragment part to encode if not null or empty.
**     CA  20210113 Renamed clear() to clear_(), to follow NameConverter's rules.
**     ME  20210205 Use a StringStringMap variable to access the internal map, use longchar for put method.
** 007 CA  20210221 Fixed 'qualified', 'extent' and 'returns' annotations at the legacy signature.
**     OM  20210323 Reworked I18n in LOB implementation.
**     VVT 20210917 Javadoc fixed.
**     OM  20210917 Code maintenance.
**     CA  20220120 Do not used TypeFactory.object when the OO reference must not be tracked or registered.
**                  Do not use TypeFactory.object for internal usages, use ObjectVar if the reference must 
**                  be tracked.
**                  All TypeFactory.object variable definitions must be done outside of the top-level block.
**     ME  20210929 Fix return object on parse/resolveRelativeReference.
**     CA  20220923 Variable definitions (including associated with parameters) must be done always outside of 
**                  the BlockManager API
** 008 HC  20230918 Changed fragment part of URI to be decoded if _p2 of parse holds true (#7653-6).
** 009 CA  20231113 The 'execute' method must be annotated with LegacySignature Type.Execute, and also can be
**                  dropped if is a no-op.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.oo.net;

import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.BlockManager.Action;
import com.goldencode.p2j.util.BlockManager.Condition;
import com.goldencode.p2j.oo.core.Assert;
import com.goldencode.p2j.oo.core.LegacyString;
import com.goldencode.p2j.oo.core.collections.StringStringMap;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.security.ContextLocal;

import static com.goldencode.p2j.util.ArrayAssigner.assignSingle;
import static com.goldencode.p2j.util.BlockManager.*;
import static com.goldencode.p2j.report.ReportConstants.*;
import static com.goldencode.p2j.util.InternalEntry.Type;

import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Set;
import java.util.stream.Collectors;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

/**
 * Business logic (converted to Java from the 4GL source code in
 * OpenEdge/Net/URI.cls).
 */
@LegacyResource(resource = "OpenEdge.Net.URI")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_PARTIAL)
public class Uri extends BaseObject
{

   private static final int _schemePartIdx = 0;
   private static final int _authorityPartIdx = 1;
   private static final int _pathPartIdx = 2;
   private static final int _queryPartIdx = 3;
   private static final int _fragmentPartIdx = 4;
   
   @LegacySignature(type = Type.PROPERTY, name = "SCHEME_PART_IDX")
   private static ContextLocal<integer> schemePartIdx = new ContextLocal<integer>()
   {
      protected integer initialValue()
      {
         return TypeFactory.integer(Long.valueOf(_schemePartIdx + 1));
      }
   };

   @LegacySignature(type = Type.PROPERTY, name = "AUTHORITY_PART_IDX")
   private static ContextLocal<integer> authorityPartIdx = new ContextLocal<integer>()
   {
      protected integer initialValue()
      {
         return TypeFactory.integer(Long.valueOf(_authorityPartIdx + 1));
      }
   };

   @LegacySignature(type = Type.PROPERTY, name = "PATH_PART_IDX")
   private static ContextLocal<integer> pathPartIdx = new ContextLocal<integer>()
   {
      protected integer initialValue()
      {
         return TypeFactory.integer(Long.valueOf(_pathPartIdx + 1));
      }
   };

   @LegacySignature(type = Type.PROPERTY, name = "QUERY_PART_IDX")
   private static ContextLocal<integer> queryPartIdx = new ContextLocal<integer>()
   {
      protected integer initialValue()
      {
         return TypeFactory.integer(Long.valueOf(_queryPartIdx + 1));
      }
   };

   @LegacySignature(type = Type.PROPERTY, name = "FRAGMENT_PART_IDX")
   private static ContextLocal<integer> fragmentPartIdx = new ContextLocal<integer>()
   {
      protected integer initialValue()
      {
         return TypeFactory.integer(Long.valueOf(_fragmentPartIdx + 1));
      }
   };

   private static BitSet defaultAllowed;
   private static BitSet schemeAllowed;
   private static BitSet userinfoAllowed;
   private static BitSet hostAllowed;
   private static BitSet pathAllowed;
   private static BitSet queryAllowed;
   private static BitSet fragmentAllowed;
   private static BitSet cookieAllowed;
   private static BitSet localAllowed;

   private enum HostType
   {
      NAME, IPV4, IPLIT
   }

   private character fragment = TypeFactory.character();

   private character host = TypeFactory.character();

   private HostType hostType;

   // default unknown
   private character password = new character();

   private character path = TypeFactory.character();

   // default unknown
   private integer port = new integer();

   private object<? extends com.goldencode.p2j.oo.core.collections.StringStringMap> queryMap = TypeFactory
            .object(com.goldencode.p2j.oo.core.collections.StringStringMap.class);

   private character scheme = TypeFactory.character();

   // default unknown
   private character user = new character();

   // encoding, from java.net.URI
   private final static char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
            'A', 'B', 'C', 'D', 'E', 'F' };

   @LegacySignature(returns = "INTEGER", type = Type.GETTER, name = "SCHEME_PART_IDX")
   public static integer getSchemePartIdx()
   {
      return function(Uri.class, "SCHEME_PART_IDX", integer.class, new Block((Body) () -> {
         returnNormal(schemePartIdx.get());
      }));
   }

   @LegacySignature(returns = "INTEGER", type = Type.GETTER, name = "AUTHORITY_PART_IDX")
   public static integer getAuthorityPartIdx()
   {
      return function(Uri.class, "AUTHORITY_PART_IDX", integer.class, new Block((Body) () -> {
         returnNormal(authorityPartIdx.get());
      }));
   }

   @LegacySignature(returns = "INTEGER", type = Type.GETTER, name = "PATH_PART_IDX")
   public static integer getPathPartIdx()
   {
      return function(Uri.class, "PATH_PART_IDX", integer.class, new Block((Body) () -> {
         returnNormal(pathPartIdx.get());
      }));
   }

   @LegacySignature(returns = "INTEGER", type = Type.GETTER, name = "QUERY_PART_IDX")
   public static integer getQueryPartIdx()
   {
      return function(Uri.class, "QUERY_PART_IDX", integer.class, new Block((Body) () -> {
         returnNormal(queryPartIdx.get());
      }));
   }

   @LegacySignature(returns = "INTEGER", type = Type.GETTER, name = "FRAGMENT_PART_IDX")
   public static integer getFragmentPartIdx()
   {
      return function(Uri.class, "FRAGMENT_PART_IDX", integer.class, new Block((Body) () -> {
         returnNormal(fragmentPartIdx.get());
      }));
   }

   private static void _appendEscape(StringBuffer sb, byte b)
   {
      sb.append('%');
      sb.append(hexDigits[(b >> 4) & 0x0f]);
      sb.append(hexDigits[(b >> 0) & 0x0f]);
   }

   private static void _appendEncoded(StringBuffer sb, char c)
   {
      ByteBuffer bb = null;
      try
      {
         bb = StandardCharsets.UTF_8.newEncoder().encode(CharBuffer.wrap("" + c));
      }
      catch (CharacterCodingException x)
      {
         assert false;
      }
      while (bb.hasRemaining())
      {
         int b = bb.get() & 0xff;
         _appendEscape(sb, (byte) b);
      }
   }

   static
   {
      defaultAllowed = new BitSet(128);
      schemeAllowed = new BitSet(128);
      userinfoAllowed = new BitSet(128);
      hostAllowed = new BitSet(128);
      pathAllowed = new BitSet(128);
      queryAllowed = new BitSet(128);
      fragmentAllowed = new BitSet(128);
      cookieAllowed = new BitSet(128);
      localAllowed = new BitSet(128);

      int i;
      for (i = 'a'; i <= 'z'; i++)
      {
         localAllowed.set(i);
      }
      for (i = 'A'; i <= 'Z'; i++)
      {
         localAllowed.set(i);
      }
      for (i = '0'; i <= '9'; i++)
      {
         localAllowed.set(i);
      }

      schemeAllowed.or(localAllowed);

      // [-,.,_,~]
      localAllowed.set('-');
      localAllowed.set('_');
      localAllowed.set('.');
      localAllowed.set('~');

      defaultAllowed.or(localAllowed);

      // [!,$,&,',(,),*,+]+[,]+[;]+[=]
      localAllowed.set('!');
      localAllowed.set('$');
      localAllowed.set('&');
      localAllowed.set(39);
      localAllowed.set('(');
      localAllowed.set(')');
      localAllowed.set('*');
      localAllowed.set('+');
      localAllowed.set(',');
      localAllowed.set(';');
      localAllowed.set('=');

      hostAllowed.or(localAllowed);
      userinfoAllowed.or(localAllowed);

      // [:,@]
      localAllowed.set(':');
      localAllowed.set('@');

      pathAllowed.or(localAllowed);
      queryAllowed.or(localAllowed);
      fragmentAllowed.or(localAllowed);

      // final touch on scheme - add [+,-,.]
      schemeAllowed.set('+');
      schemeAllowed.set('-');
      schemeAllowed.set('.');

      // final touch on userInfo - add [:]
      userinfoAllowed.set(':');

      // final touch on query - add [/,?] remove [&]
      queryAllowed.set('/');
      queryAllowed.set('?');
      queryAllowed.clear('&');

      // TODO: in OE 1.7, queryAllowed must contain '=' char; in OE 1.6, '=' gets encoded.

      // final touch on fragment - add [/,?]
      fragmentAllowed.set('/');
      fragmentAllowed.set('?');

      // cookie
      for (i = 33; i <= 126; i++)
      {
         cookieAllowed.set(i);
      }
      cookieAllowed.clear(34);
      cookieAllowed.clear(44);
      cookieAllowed.clear(59);
      cookieAllowed.clear(92);

   }

   @LegacySignature(type = Type.EXECUTE)
   public void __net_Uri_execute__()
   {
      onBlockLevel(Condition.ERROR, Action.THROW);
   }

   @LegacySignature(returns = "CHARACTER", type = Type.GETTER, name = "BaseURI")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public character getBaseUri()
   {
      return function(this, "BaseURI", character.class, new Block((Body) () -> {
         returnNormal(new character(_getBaseUri()));
      }));
   }

   public String _getBaseUri()
   {
      StringBuilder sb = new StringBuilder(getScheme().getValue()).append("://");

      sb.append(_getAuthority());

      return sb.toString();

   }

   @LegacySignature(returns = "CHARACTER", type = Type.GETTER, name = "Fragment")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public character getFragment()
   {
      return function(this, "Fragment", character.class, new Block((Body) () -> {
         returnNormal(fragment);
      }));
      
   }

   @LegacySignature(type = Type.SETTER, name = "Fragment", parameters = {
            @LegacyParameter(name = "_var", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | CVT_LVL_FULL)
   public void setFragment(final character _var)
   {
      character var = TypeFactory.initInput(_var);
      
      internalProcedure(Uri.class, this, "Fragment", new Block((Body) () -> 
      {
         fragment.assign(var.isUnknown() ? "" : var);
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.GETTER, name = "Host")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public character getHost()
   {
      return function(this, "Host", character.class, new Block((Body) () -> {
         returnNormal(host);
      }));
   }

   protected String _getAuthority() {
      StringBuilder sb = new StringBuilder();

      if (!user.isUnknown())
      {
         sb.append(user.getValue());
         if (!password.isUnknown())
         {
            sb.append(":").append(getPassword().getValue());
         }
         sb.append("@");
      }
      
      if (hostType.equals(HostType.IPLIT))
      {
         sb.append("[").append(host.getValue()).append("]");
      }
      else
      {
         sb.append(host.getValue());
      }
      
      if (!port.isUnknown())
      {
         sb.append(":").append(getPort().intValue());
      }
      
      return sb.toString();
   }
   
   private void _setHost(final character host)
   {
      if (host.getValue().startsWith("["))
      {
         this.hostType = HostType.IPLIT;
         int endPos = host.getValue().indexOf("]");
         // 4gl implementation doesn't enforce closing element, 
         // it actually trims everything after first closing bracket 
         this.host.assign(_substring(host.getValue(), 1, endPos));
      }
      else
      {
         this.hostType = HostType.NAME;
         this.host.assign(host);
      }
   }

   @LegacySignature(returns = "CHARACTER", type = Type.GETTER, name = "Password")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public character getPassword()
   {
      return function(this, "Password", character.class, new Block((Body) () -> {
         returnNormal(password);
      }));
   }

   @LegacySignature(type = Type.SETTER, name = "Password", parameters = {
            @LegacyParameter(name = "_var", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | CVT_LVL_FULL)
   public void setPassword(final character _var)
   {
      character var = TypeFactory.initInput(_var);
      
      internalProcedure(Uri.class, this, "Password", new Block((Body) () -> 
      {
         password.assign(var);
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.GETTER, name = "Path")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public character getPath()
   {
      return function(this, "Path", character.class, new Block((Body) () -> {
         returnNormal(path);
      }));
   }

   @LegacySignature(type = Type.SETTER, name = "Path", parameters = {
            @LegacyParameter(name = "_pPath", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public void setPath(final character _pPath)
   {
      character pPath = TypeFactory.initInput(_pPath);
      
      internalProcedure(Uri.class, this, "Path", new Block((Body) () -> 
      {
         Assert.notNullOrEmpty(pPath, new character("path"));
         
         // no validation nor encoding, this is probably a bug and should just call protected setPath method instead
         String newPath = pPath.getValue();
         
         if (!newPath.startsWith("/"))
            newPath = "/" + newPath;
         
         path.assign(newPath);
      }));
   }

   @LegacySignature(type = Type.METHOD, name = "SetPath", parameters = {
            @LegacyParameter(name = "_pPath", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   protected void setPath_1(final character _pPath)
   {
      character pPath = TypeFactory.initInput(_pPath);
      
      internalProcedure(Uri.class, this, "SetPath", new Block((Body) () -> 
      {
         String newPath = pPath.isUnknown() ? "/" : pPath.getValue();
         
         if (!newPath.startsWith("/"))
            newPath = "/" + newPath;
         
         if (newPath.length() > 1) {
            String[] entries = TextOps.entries(newPath, "/");
            
            newPath = Arrays.stream(entries).map(p -> _encodeString(p, null, pathAllowed, false)).collect(Collectors.joining("/"));
         }
         
         setPath(new character(newPath));
      }));
   }

   @LegacySignature(returns = "INTEGER", type = Type.GETTER, name = "Port")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public integer getPort()
   {
      return function(this, "Port", integer.class, new Block((Body) () -> {
         returnNormal(port);
      }));
   }

   @LegacySignature(type = Type.SETTER, name = "Port", parameters = {
            @LegacyParameter(name = "_var", type = "INTEGER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public void setPort(final integer _var)
   {
      integer var = TypeFactory.initInput(_var);
      
      internalProcedure(Uri.class, this, "Port", new Block((Body) () -> 
      {
         port.assign(var);
      }));
   }

   @LegacySignature(returns = "OBJECT", qualified = "OpenEdge.Core.Collections.IStringStringMap", type = Type.GETTER, name = "QueryMap")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   protected object<? extends com.goldencode.p2j.oo.core.collections.IStringStringMap> getQueryMap()
   {
      return function(this, "QueryMap", object.class, new Block((Body) () -> {
         if (queryMap.isUnknown())
            queryMap.assign(ObjectOps.newInstance(StringStringMap.class));

         returnNormal(queryMap);
      }));
   }

   @LegacySignature(returns = "OBJECT", type = Type.METHOD, name = "GetQueryMap", qualified = "openedge.core.collections.istringstringmap")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public object<? extends com.goldencode.p2j.oo.core.collections.IStringStringMap> getQueryMap_1()
   {
      return function(this, "GetQueryMap", object.class, new Block((Body) () -> {
         returnNormal(ObjectOps.newInstance(StringStringMap.class, "I", getQueryMap()));
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.GETTER, name = "QueryString")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public character getQueryString()
   {
      return function(this, "QueryString", character.class, new Block((Body) () -> {
         returnNormal(new character(_getQueryString(false)));
      }));
   }

   private void _setQueryString(final character var)
   {
      getQueryMap().ref().clear_();
      addQueryString(var, new logical(true));
   }

   public String _getQueryString(boolean encode)
   {
      if (queryMap.isUnknown() || queryMap.ref().isEmpty().booleanValue())
      {
         return "";
      }

      String qryString = queryMap.ref().getMap().entrySet().stream().map(e -> {
         LegacyString key = (LegacyString) e.getKey().ref();
         LegacyString value = (LegacyString) e.getValue().ref();
         
         String qry = key.toLegacyString().getValue();
         
         if (!value.getValue().isUnknown())
            qry += "=";
         
         if (!value.isNullOrEmpty().booleanValue())
            qry += value.toLegacyString().getValue();

         return encode ? encodeQuery(qry) : qry;
      }).reduce("?", (s, e) -> {
         if ("?".equals(s))
            return s + e;

         return s + "&" + e;
      });

      return qryString;
   }

   @LegacySignature(returns = "CHARACTER", type = Type.GETTER, name = "RelativeURI")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public character getRelativeUri()
   {
      return function(this, "RelativeURI", character.class, new Block((Body) () -> {
         returnNormal(new character(_getRelativeUri()));
      }));
   }

   public String _getRelativeUri()
   {
      StringBuilder sb = new StringBuilder(path.getValue());
      String extra = _getQueryString(false);

      if (!extra.isEmpty())
         sb.append(extra);

      extra = getFragment().getValue();
      if (extra != null && !extra.isEmpty())
         sb.append("#").append(extra);

      return sb.toString();
   }

   @LegacySignature(returns = "CHARACTER", type = Type.GETTER, name = "Scheme")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public character getScheme()
   {
      return function(this, "Scheme", character.class, new Block((Body) () -> {
         returnNormal(scheme);
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.GETTER, name = "User")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public character getUser()
   {
      return function(this, "User", character.class, new Block((Body) () -> {
         returnNormal(user);
      }));
   }

   @LegacySignature(type = Type.SETTER, name = "User", parameters = {
            @LegacyParameter(name = "_var", type = "CHARACTER", mode = "INPUT") })
   public void setUser(final character _var)
   {
      character var = TypeFactory.initInput(_var);
      
      internalProcedure(Uri.class, this, "User", new Block((Body) () -> 
      {
         user.assign(var);
      }));
   }

   @LegacySignature(type = Type.CONSTRUCTOR)
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static void __net_Uri_constructor__static__()
   {
      externalProcedure(Uri.class, new Block((Body) () ->
      {
         onBlockLevel(Condition.ERROR, Action.THROW);
      }));
   }

   @LegacySignature(type = Type.CONSTRUCTOR, parameters = {
            @LegacyParameter(name = "host", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public void __net_Uri_constructor__(final character _host)
   {
      __net_Uri_constructor__(UriSchemeEnum.http.ref().toLegacyString(), _host);
   }

   @LegacySignature(type = Type.CONSTRUCTOR, parameters = {
            @LegacyParameter(name = "scheme", type = "CHARACTER", mode = "INPUT"),
            @LegacyParameter(name = "host", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public void __net_Uri_constructor__(final character _scheme, final character _host)
   {
      __net_Uri_constructor__(_scheme, _host, new integer());
   }

   @LegacySignature(type = Type.CONSTRUCTOR, parameters = {
            @LegacyParameter(name = "scheme", type = "CHARACTER", mode = "INPUT"),
            @LegacyParameter(name = "host", type = "CHARACTER", mode = "INPUT"),
            @LegacyParameter(name = "port", type = "INTEGER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public void __net_Uri_constructor__(final character _scheme, final character _host,
            final integer _port)
   {
      __net_Uri_constructor__(_scheme, _host, _port, new character("/"),
               ObjectOps.newInstance(StringStringMap.class), TypeFactory.character());
   }

   @LegacySignature(type = Type.CONSTRUCTOR, parameters = {
            @LegacyParameter(name = "scheme", type = "CHARACTER", mode = "INPUT"),
            @LegacyParameter(name = "host", type = "CHARACTER", mode = "INPUT"),
            @LegacyParameter(name = "port", type = "INTEGER", mode = "INPUT"),
            @LegacyParameter(name = "path", type = "CHARACTER", mode = "INPUT"),
            @LegacyParameter(name = "query", type = "OBJECT", qualified = "openedge.core.collections.istringstringmap", mode = "INPUT"),
            @LegacyParameter(name = "fragment", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public void __net_Uri_constructor__(final character _scheme, final character _host,
            final integer _port, final character _path,
            final object<? extends com.goldencode.p2j.oo.core.collections.IStringStringMap> _query,
            final character _fragment)
   {
      character scheme = TypeFactory.initInput(_scheme);
      character host = TypeFactory.initInput(_host);
      integer port = TypeFactory.initInput(_port);
      character path = TypeFactory.initInput(_path);
      object<? extends com.goldencode.p2j.oo.core.collections.IStringStringMap> query = TypeFactory
               .initInput(_query);
      character fragment = TypeFactory.initInput(_fragment);

      internalProcedure(this, "__net_Uri_constructor__", new Block((Body) () -> {
         __lang_BaseObject_constructor__();
         Assert.notNullOrEmpty(scheme, new character("Scheme"));
         Assert.notNullOrEmpty(host, new character("Host"));
         Assert.notNull(query, new character("QueryMap"));
         Assert.notNullOrEmpty(path, new character("Path"));

         this.scheme.assign(scheme);
         this.queryMap.assign(query);

         _setHost(host);
         setPort(port);
         setPath(path);
         setFragment(fragment);
      }));
   }

   @LegacySignature(type = Type.CONSTRUCTOR, parameters = {
            @LegacyParameter(name = "pScheme", type = "CHARACTER", mode = "INPUT"),
            @LegacyParameter(name = "pAuthority", type = "CHARACTER", mode = "INPUT"),
            @LegacyParameter(name = "pPath", type = "CHARACTER", mode = "INPUT"),
            @LegacyParameter(name = "pQuery", type = "CHARACTER", mode = "INPUT"),
            @LegacyParameter(name = "pFragment", type = "CHARACTER", mode = "INPUT") })
   public void __net_Uri_constructor__(final character pScheme, final character pAuthority,
            final character pPath, final character pQuery, final character pFragment)
   {
      character scheme = TypeFactory.initInput(pScheme);
      character authority = TypeFactory.initInput(pAuthority);
      character path = TypeFactory.initInput(pPath);
      character fragment = TypeFactory.initInput(pFragment);
      character query = TypeFactory.initInput(pQuery);
      
      internalProcedure(this, "__net_Uri_constructor__", new Block((Body) () -> {
         __lang_BaseObject_constructor__();
         Assert.notNullOrEmpty(scheme, new character("URI Scheme"));
         this.scheme.assign(scheme);
   
         setAuthority(authority, new logical(true));
   
         setPath_1(path);
   
         addQueryString(query, new logical(true));
         setFragment(fragment);
      }));
   }

   @LegacySignature(type = Type.METHOD, name = "AddPathSegment", parameters = {
            @LegacyParameter(name = "_p1", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | CVT_LVL_FULL)
   public void addPathSegment(final character _p1)
   {
      character p1 = TypeFactory.initInput(_p1);
      
      internalProcedure(this, "AddPathSegment", new Block((Body) () -> {
         character seg = encodePath(p1);
   
         if (!seg.getValue().trim().isEmpty())
         {
            if (this.path.getValue().endsWith("/"))
               this.path.assign(this.path.getValue() + seg.getValue());
            else
               this.path.assign(this.path.getValue() + "/" + seg.getValue());
   
         }
      }));
   }

   @LegacySignature(type = Type.METHOD, name = "AddQuery", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public void addQuery(final character _p1)
   {
      addQuery(_p1, new character());
   }

   @LegacySignature(type = Type.METHOD, name = "AddQuery", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT"),
            @LegacyParameter(name = "p2", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public void addQuery(final character _p1, final character _p2)
   {
      character p1 = TypeFactory.initInput(_p1);
      character p2 = TypeFactory.initInput(_p2);

      internalProcedure(this, "AddQuery", new Block((Body) () -> {
         Assert.notNullOrEmpty(p1, new character("Query name"));
         getQueryMap().ref().put(p1, p2.isUnknown() ? new longchar() : new longchar(encodeQuery(p2)));
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "Decode")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public character decode()
   {
      return function(this, "Decode", character.class, new Block((Body) () -> {
         returnNormal(decode(this.toLegacyString()));
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "Decode", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character decode(final character p1)
   {
      return function(Uri.class, "Decode", character.class, new Block((Body) () -> {
         returnNormal(decode(p1, new character()));
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "Decode", parameters = {
            @LegacyParameter(name = "data", type = "CHARACTER", mode = "INPUT"),
            @LegacyParameter(name = "encoding", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character decode(final character _data, final character _encoding)
   {
      return function(Uri.class, "Decode", character.class, new Block((Body) () -> {
         String data = _data.isUnknown() ? "" : _data.toStringMessage();
         String encoding = _encoding.isUnknown() ? "" : _encoding.getValue().trim();
         
         if (data.trim().isEmpty())
         {
            returnNormal(_data);
         }
         
         try
         {
            String uri = _decode(data);
            // in FWD, the [character] always uses the internal CP, CP conversion is a NOP
            returnNormal(new character(uri));
         }
         catch (UnsupportedEncodingException e)
         {
            undoThrow(AppError.newInstance(String.format("Cannot decode malformed string %s", data), 0));
         }
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "Decode", parameters = {
            @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.core.string", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public static character decode(
            final object<? extends com.goldencode.p2j.oo.core.LegacyString> _p1)
   {
      object<? extends com.goldencode.p2j.oo.core.LegacyString> p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "Decode", character.class, new Block((Body) () -> {
         returnNormal(decode(p1.ref().toLegacyString(), p1.ref().getEncoding()));
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "Encode")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public character encode()
   {
      return function(this, "Encode", character.class, new Block((Body) () -> {
         returnNormal(encodeUri(new object(this)));
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "Encode", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT"),
            @LegacyParameter(name = "p2", type = "OBJECT", qualified = "openedge.net.uriencodingtypeenum", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public static character encode(final character p1,
            final object<? extends com.goldencode.p2j.oo.net.UriEncodingTypeEnum> p2)
   {
      return encode(p1, p2, new logical(false));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "Encode", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT"),
            @LegacyParameter(name = "p2", type = "OBJECT", qualified = "openedge.net.uriencodingtypeenum", mode = "INPUT"),
            @LegacyParameter(name = "p3", type = "LOGICAL", mode = "INPUT") })
   public static character encode(final character _p1,
            final object<? extends com.goldencode.p2j.oo.net.UriEncodingTypeEnum> _p2,
            final logical _p3)
   {
      character p1 = TypeFactory.initInput(_p1);
      object<? extends com.goldencode.p2j.oo.net.UriEncodingTypeEnum> p2 = TypeFactory
               .initInput(_p2);
      logical reEncode = TypeFactory.initInput(_p3);

      return function(Uri.class, "Encode", character.class, new Block((Body) () -> {

         if (p1.isUnknown() || p1.getValue().trim().isEmpty())
            returnNormal(new character(""));

         if (reEncode.isUnknown())
         {
            reEncode.assign(true);
         }

         if (!p2._isValid())
         {
            returnNormal(new character(
                     _encodeString(p1.getValue(), "?", defaultAllowed, reEncode.booleanValue())));
         }

         switch (p2.ref().getValue().toString())
         {
            case "1":
               //scheme
               returnNormal(new character(_encodeString(p1.getValue(), "?", schemeAllowed,
                        reEncode.booleanValue())));

            case "2":
               //host
               returnNormal(new character(
                        _encodeString(p1.getValue(), "?", hostAllowed, reEncode.booleanValue())));

            case "3":
               //path
               returnNormal(new character(
                        _encodeString(p1.getValue(), "?", pathAllowed, reEncode.booleanValue())));

            case "4":
               //query
               returnNormal(new character(
                        _encodeString(p1.getValue(), "?", queryAllowed, reEncode.booleanValue())));

            case "5":
               //fragment
               returnNormal(new character(_encodeString(p1.getValue(), "?", fragmentAllowed,
                        reEncode.booleanValue())));

            case "6":
               //cookie
               returnNormal(new character(_encodeString(p1.getValue(), "?", cookieAllowed,
                        reEncode.booleanValue())));

            default:
               returnNormal(new character(_encodeString(p1.getValue(), "?", defaultAllowed,
                        reEncode.booleanValue())));
         }

      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "Encode", parameters = {
            @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.net.uri", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public static character encode(final object<? extends com.goldencode.p2j.oo.net.Uri> p1)
   {
      return encodeUri(p1);
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeCookie", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodeCookie(final character _p1)
   {
      character p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "EncodeCookie", character.class, new Block((Body) () -> {
         returnNormal(_encodeString(p1.getValue(), "?", cookieAllowed, false));
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeCookie", parameters = {
            @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.core.string", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodeCookie(
            final object<? extends com.goldencode.p2j.oo.core.LegacyString> _p1)
   {
      object<? extends com.goldencode.p2j.oo.core.LegacyString> p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "EncodeCookie", character.class, new Block((Body) () -> {
         returnNormal(encodeCookie(p1.ref().toLegacyString().getValue(),
                  p1.ref().getEncoding().getValue()));
      }));
   }

   public static String encodeCookie(final String data, final String codePage)
   {
      return _encodeString(data, codePage, cookieAllowed, false);
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeFragment")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public character encodeFragment()
   {
      return function(this, "EncodeFragment", character.class, new Block((Body) () -> {

         returnNormal(encodeFragment(this.fragment));

      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeFragment", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodeFragment(final character _p1)
   {
      character p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "EncodeFragment", character.class, new Block((Body) () -> {

         returnNormal(_encodeString(p1.getValue(), "?", fragmentAllowed, false));

      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeFragment", parameters = {
            @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.core.string", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodeFragment(
            final object<? extends com.goldencode.p2j.oo.core.LegacyString> _p1)
   {
      object<? extends com.goldencode.p2j.oo.core.LegacyString> p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "EncodeFragment", character.class, new Block((Body) () -> {
         returnNormal(_encodeString(p1.ref().toLegacyString().getValue(),
                  p1.ref().getEncoding().getValue(), fragmentAllowed, false));
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeHost")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public character encodeHost()
   {
      return function(this, "EncodeHost", character.class, new Block((Body) () -> {

         if (this.hostType == HostType.IPLIT)
         {
            returnNormal(new character("[" + encodeHost(this.host).getValue() + "]"));
         }
         else
            returnNormal(encodeHost(this.host));

      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeHost", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodeHost(final character _p1)
   {
      character p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "EncodeHost", character.class, new Block((Body) () -> {

         returnNormal(_encodeString(p1.getValue(), "?", hostAllowed, false));

      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeHost", parameters = {
            @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.core.string", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodeHost(
            final object<? extends com.goldencode.p2j.oo.core.LegacyString> _p1)
   {
      object<? extends com.goldencode.p2j.oo.core.LegacyString> p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "EncodeHost", character.class, new Block((Body) () -> {
         returnNormal(_encodeString(p1.ref().toLegacyString().getValue(),
                  p1.ref().getEncoding().getValue(), hostAllowed, false));
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodePath", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodePath(final character p1)
   {
      return new character(_encodeString(p1.getValue(), "?", pathAllowed, false));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodePath", parameters = {
            @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.core.string", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodePath(
            final object<? extends com.goldencode.p2j.oo.core.LegacyString> _p1)
   {
      object<? extends com.goldencode.p2j.oo.core.LegacyString> p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "EncodePath", character.class, new Block((Body) () -> {
         returnNormal(_encodeString(p1.ref().toLegacyString().getValue(),
                  p1.ref().getEncoding().getValue(), pathAllowed, false));
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeQuery")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public character encodeQuery()
   {
      return function(this, "EncodeQuery", character.class, new Block((Body) () -> {
         returnNormal(encodeQuery_1(new object(this)));
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeQuery", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodeQuery(final character _p1)
   {
      character p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "EncodeQuery", character.class, new Block((Body) () -> {
         returnNormal(encodeQuery(p1.getValue()));
      }));
   }

   public static String encodeQuery(String query) {
      return _encodeString(query, "?", queryAllowed, false);
   }
   
   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeQuery", parameters = {
            @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.core.string", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodeQuery(
            final object<? extends com.goldencode.p2j.oo.core.LegacyString> _p1)
   {
      object<? extends com.goldencode.p2j.oo.core.LegacyString> p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "EncodeQuery", character.class, new Block((Body) () -> {
         returnNormal(_encodeString(p1.ref().toLegacyString().getValue(),
                  p1.ref().getEncoding().getValue(), queryAllowed, false));
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeQuery", parameters = {
            @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.net.uri", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodeQuery_1(
            final object<? extends com.goldencode.p2j.oo.net.Uri> _p1)
   {
      object<? extends com.goldencode.p2j.oo.net.Uri> p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "EncodeQuery", character.class, new Block((Body) () -> {

         Assert.notNull(p1, new character("URI"));
         
         returnNormal(p1.ref()._getQueryString(true));
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeScheme")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public character encodeScheme()
   {
      return encodeScheme(this.scheme);
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeScheme", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodeScheme(final character _p1)
   {
      character p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "EncodeScheme", character.class, new Block((Body) () -> {

         returnNormal(_encodeString(p1.getValue(), "?", schemeAllowed, false));

      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeScheme", parameters = {
            @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.core.string", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodeScheme(
            final object<? extends com.goldencode.p2j.oo.core.LegacyString> _p1)
   {
      object<? extends com.goldencode.p2j.oo.core.LegacyString> p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "EncodeScheme", character.class, new Block((Body) () -> {

         returnNormal(_encodeString(p1.ref().toLegacyString().getValue(),
                  p1.ref().getEncoding().getValue(), schemeAllowed, false));

      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeString", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodeString(final character _p1)
   {
      character p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "EncodeString", character.class, new Block((Body) () -> {

         returnNormal(_encodeString(p1.getValue(), "?", defaultAllowed, false));

      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeString", parameters = {
            @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.core.string", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodeString(
            final object<? extends com.goldencode.p2j.oo.core.LegacyString> _p1)
   {
      object<? extends com.goldencode.p2j.oo.core.LegacyString> p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "EncodeString", character.class, new Block((Body) () -> {
         returnNormal(_encodeString(p1.ref().toLegacyString().getValue(),
                  p1.ref().getEncoding().getValue(), defaultAllowed, false));
      }));
   }

   private static String _encodeString(String str, String sourceCodepage, BitSet allowed,
            boolean reEncode)
   {

      StringBuffer sb = null;

      if (str == null || str.trim().isEmpty())
         return "";

      if (sourceCodepage == null)
         sourceCodepage = I18nOps.getCPInternal().getValue();

      // TODO: codepage conversion
      // str = I18nOps.codePageConvertWorker(str, sourceCodepage, "UTF-8");

      for (int i = 0; i < str.length(); i++)
      {
         char c = str.charAt(i);

         if (c == '\u0025' && !reEncode)
         {
            if (sb != null)
            {
               sb.append(str.substring(i, Math.min(i + 3, str.length())));
            }
            i = i + 2;
         }
         else if (c >= '\u007F' || c <= '\u001F' || !allowed.get(c))
         {
            if (sb == null)
            {
               sb = new StringBuffer();
               sb.append(str.substring(0, i));
            }

            _appendEncoded(sb, c);
         }
         else
         {
            if (sb != null)
            {
               sb.append(c);
            }
         }
      }

      return (sb == null) ? str : sb.toString();
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeURI", parameters = {
            @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.net.uri", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodeUri(final object<? extends com.goldencode.p2j.oo.net.Uri> _p1)
   {
      object<? extends com.goldencode.p2j.oo.net.Uri> p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "EncodeURI", character.class, new Block((Body) () -> {

         String encodedUri = new String();

         Assert.notNull(p1, new character("URI"));

         //scheme
         encodedUri = encodeScheme(p1.ref().getScheme()).getValue() + "://";

         //user+pass
         if (!p1.ref().getUser().isUnknown())
         {
            encodedUri = encodedUri + p1.ref().encodeUserinfo().getValue() + "@";
         }

         //host
         encodedUri = encodedUri + p1.ref().encodeHost().getValue();

         //port
         if (!p1.ref().getPort().isUnknown())
         {
            encodedUri = encodedUri + ":" + p1.ref().getPort().getValue();
         }

         //path + query
         encodedUri = encodedUri + p1.ref().getPath().getValue()
                  + p1.ref().encodeQuery().getValue();

         //fragment
         if (!TextOps.isEmpty(p1.ref().getFragment()))
         {
            encodedUri = encodedUri + "#" + encodeFragment(p1.ref().getFragment()).getValue();
         }

         returnNormal(new character(encodedUri));
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeUserinfo")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public character encodeUserinfo()
   {
      return function(Uri.class, "EncodeUserinfo", character.class, new Block((Body) () -> {

         if (this.user.isUnknown())
         {
            returnNormal(new character(""));
         }

         character u = this.getUser();

         String authority = new String(_encodeString(u.getValue(), "?", hostAllowed, false));

         if (!this.password.isUnknown())
         {
            authority = authority + ":"
                     + _encodeString(this.getPassword().getValue(), "?", hostAllowed, false);
         }

         returnNormal(new character(authority));

      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeUserinfo", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodeUserinfo(final character _p1)
   {
      character p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "EncodeUserinfo", character.class, new Block((Body) () -> {

         returnNormal(_encodeString(p1.getValue(), "?", userinfoAllowed, false));

      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "EncodeUserinfo", parameters = {
            @LegacyParameter(name = "p1", type = "OBJECT", qualified = "openedge.core.string", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character encodeUserinfo(
            final object<? extends com.goldencode.p2j.oo.core.LegacyString> _p1)
   {
      object<? extends com.goldencode.p2j.oo.core.LegacyString> p1 = TypeFactory.initInput(_p1);

      return function(Uri.class, "EncodeUserinfo", character.class, new Block((Body) () -> {
         returnNormal(_encodeString(p1.ref().toLegacyString().getValue(),
                  p1.ref().getEncoding().getValue(), userinfoAllowed, false));
      }));
   }

   @LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "GetPathSegments", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", extent = -1, mode = "OUTPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public integer getPathSegments(final OutputExtentParameter<character> extp1)
   {
      character[][] p1 = { TypeFactory.initOutput(extp1) };

      return function(this, "GetPathSegments", integer.class, new Block((Body) () -> {
         String[] entries = TextOps.entries(this.path.getValue(), "/");

         p1[0] = ArrayAssigner.resize(p1[0], entries.length - 1);

         for (int i = 0; i < p1[0].length; i++)
         {
            assignSingle(p1[0], i + 1, decode(new character(entries[i + 1])));
         }
         
         returnNormal(p1[0].length);
      }));
   }

   @LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "GetQueryNames", parameters = {
            @LegacyParameter(name = "np1", type = "CHARACTER", extent = -1, mode = "OUTPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public integer getQueryNames(final OutputExtentParameter<character> extnp1)
   {
      character[][] np1 = { TypeFactory.initOutput(extnp1) };

      return function(this, "GetQueryNames", integer.class, new Block((Body) () -> {
         if (queryMap.isUnknown())
         {
            returnNormal(0);
         }
         else
         {
            Set<object> keys = this.queryMap.ref().getMap().keySet();

            if (!keys.isEmpty())
            {
               np1[0] = ArrayAssigner.resize(np1[0], keys.size());

               int idx = 0;

               for (object key : keys)
               {
                  assignSingle(np1[0], ++idx, key.toString());
               }
            }
            returnNormal(keys.size());
         }
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "GetQueryValue", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public character getQueryValue(final character _p1)
   {
      character p1 = TypeFactory.initInput(_p1);

      return function(this, "GetQueryValue", character.class, new Block((Body) () -> {
         Assert.notNullOrEmpty(p1, new character("Query name"));

         if (queryMap.isUnknown())
         {
            returnNormal(new character());
         }
         else
         {
            returnNormal(new character(this.queryMap.ref().get(p1)));
         }
      }));
   }

   @LegacySignature(returns = "LOGICAL", type = Type.METHOD, name = "HasQueryName", parameters = {
            @LegacyParameter(name = "pName", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public logical hasQueryName(final character pName)
   {
      return function(Uri.class, this, "HasQueryName", logical.class, new Block((Body) () -> {
         returnNormal(!queryMap.isUnknown() && queryMap.ref().containsKey(pName).booleanValue());
      }));
   }

   @LegacySignature(returns = "OBJECT", type = Type.METHOD, name = "Parse", qualified = "openedge.net.uri", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_BASIC)
   public static object<? extends com.goldencode.p2j.oo.net.Uri> parse(final character _p1)
   {
      return parse(_p1, new logical(true));
   }

   @LegacySignature(returns = "OBJECT", type = Type.METHOD, name = "Parse", qualified = "openedge.net.uri", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT"),
            @LegacyParameter(name = "p2", type = "LOGICAL", mode = "INPUT") })
   public static object<? extends Uri> parse(final character _p1, final logical _p2)
   {

      character p1 = TypeFactory.initInput(_p1);
      logical p2 = TypeFactory.initInput(_p2);
      object<Uri> obj = TypeFactory.object(Uri.class);

      return function(Uri.class, "Parse", object.class, new Block((Body) () -> {
         Assert.notNullOrEmpty(p1, new character("URI string"));

         character[] relative = splitUri(p1);

         // defaults to HTTP
         if (TextOps.isEmpty(relative[_schemePartIdx]))
            relative[_schemePartIdx] = UriSchemeEnum.http.ref().toLegacyString();
         
         // check authority as this is mandatory
         if (TextOps.isEmpty(relative[_authorityPartIdx]))
            undoThrow(AppError.newInstance(TextOps.substitute("Unable to parse malformed URI: &1", p1), new integer(0)));
         
         // authority seems to always be decoded
         relative[_authorityPartIdx] = decode(relative[_authorityPartIdx]);
         
         // unless false decode by default schema, path, query, fragment
         if (p2.isUnknown() || p2.booleanValue())
         {
            relative[_schemePartIdx] = decode(relative[_schemePartIdx]);
            relative[_pathPartIdx] = decode(relative[_pathPartIdx]);
            relative[_fragmentPartIdx] = decode(relative[_fragmentPartIdx]);
            relative[_queryPartIdx] = decode(relative[_queryPartIdx]);

            obj.assign(ObjectOps.newInstance(Uri.class, "IIIII", relative[_schemePartIdx],
                     relative[_authorityPartIdx], relative[_pathPartIdx], relative[_queryPartIdx],
                     relative[_fragmentPartIdx]));
         }
         else
         {
            // don't set query string in ctor to avoid decode
            obj.assign(ObjectOps.newInstance(Uri.class, "IIIII", relative[_schemePartIdx],
                     relative[_authorityPartIdx], relative[_pathPartIdx], "",
                     relative[_fragmentPartIdx]));
            
            obj.ref().addQueryString(relative[_queryPartIdx], new logical(false));
         }
         
         returnNormal(obj);
      }));
   }

   @LegacySignature(returns = "LOGICAL", type = Type.METHOD, name = "RemoveQuery", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public logical removeQuery(final character _p1)
   {
      character p1 = TypeFactory.initInput(_p1);

      return function(this, "RemoveQuery", logical.class, new Block((Body) () -> {

         Assert.notNullOrEmpty(p1, new character("Query name"));

         boolean hasQuery = !queryMap.isUnknown() && queryMap.ref().containsKey(p1).booleanValue();

         if (hasQuery)
            this.queryMap.ref().remove(p1);

         returnNormal(hasQuery);
      }));
   }

   @LegacySignature(type = Type.METHOD, name = "SetPath", parameters = {
            @LegacyParameter(name = "p1", type = "CHARACTER", extent = -1, mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public void setPath(final character[] _p1)
   {
      character[] p1 = TypeFactory.initInput(_p1);

      internalProcedure(this, "SetPath", new Block((Body) () -> {
         String path = new String();
         String pathDelim = "";
         for (character a : p1)
         {
            String partialSeg = encodePath(a).getValue();
            if (partialSeg != "")
            {
               if (path == "")
               {
                  path = "/";
               }
               path = path + pathDelim + partialSeg;
               pathDelim = "/";
            }
         }
         setPath(new character(path));
      }));
   }

   @LegacySignature(type = Type.METHOD, name = "SetQueryMap", parameters = {
            @LegacyParameter(name = "pQueryMap", type = "OBJECT", qualified = "openedge.core.collections.istringstringmap", mode = "INPUT") })
   public void setQueryMap_1(
            final object<? extends com.goldencode.p2j.oo.core.collections.IStringStringMap> _pQueryMap)
   {
      object<? extends com.goldencode.p2j.oo.core.collections.IStringStringMap> pQueryMap = TypeFactory
               .initInput(_pQueryMap);

      internalProcedure(Uri.class, this, "SetQueryMap", new Block((Body) () -> {
         if (_pQueryMap._isValid() && pQueryMap.ref().getSize().getSize() > 0)
         {
            getQueryMap().ref().putAll_2(pQueryMap);
         }
      }));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "ToString")
   @Override
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public character toLegacyString()
   {
      return new character(_getBaseUri() + _getRelativeUri());
   }

   @LegacySignature(type = Type.METHOD, name = "SetAuthority", parameters = {
            @LegacyParameter(name = "pAuthority", type = "CHARACTER", mode = "INPUT"),
            @LegacyParameter(name = "pDecode", type = "LOGICAL", mode = "INPUT") })
   protected void setAuthority(final character pAuthority, final logical pDecode)
   {
      Assert.notNullOrEmpty(pAuthority, new character("Authority"));

      String auth = pAuthority.getValue();
      int idx = auth.indexOf("@");

      // credentials set (user:passwd)
      if (idx != -1)
      {
         String[] credentials = auth.substring(0, idx).split(":");
         user.assign(credentials[0]);

         if (credentials.length > 1)
            password.assign(credentials[1]);

         auth = auth.substring(idx + 1);
      }

      idx = auth.indexOf(":");
      if (idx != -1)
      {
         port.assign(auth.substring(idx + 1));
         auth = auth.substring(0, idx);
      }

      _setHost(pDecode.isUnknown() || pDecode.booleanValue() ? decode(new character(auth)) : new character(auth));
   }

   @LegacySignature(type = Type.METHOD, name = "AddQueryString", parameters = {
            @LegacyParameter(name = "pQuery", type = "CHARACTER", mode = "INPUT"),
            @LegacyParameter(name = "pDecode", type = "LOGICAL", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public void addQueryString(character pQuery, logical pDecode)
   {
      if (!pQuery.isUnknown() && !pQuery.getValue().trim().isEmpty())
      {
         Arrays.stream(pQuery.getValue().split("&")).forEach(e -> {
            String[] entries = e.split("=");
            character pName = pDecode.booleanValue() ? decode(new character(entries[0])) : new character(entries[0]); 
            if (entries.length == 1)
               addQuery(pName);
            else
               addQuery(pName, new character(entries[1]));
         });
      }
   }

   /**
    * Parses this URI into its component parts
    * [1] scheme (http/https)
    * [2] authority (host:port)
    * [3] path
    * [4] query
    * [5] fragment

    * @return An array of component parts
    */
   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "Split", extent = -1)
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public character[] split()
   {
      return extentFunction(Uri.class, this, "Split", character.class, new Block((Body) () -> {
         String[] entries = new String[] {scheme.getValue(), _getAuthority(), path.getValue(), _getQueryString(false), fragment.getValue()};
         
         if (entries[_queryPartIdx].startsWith("?"))
            entries[_queryPartIdx] = entries[_queryPartIdx].substring(1);
         
         returnExtentNormal(TypeFactory.characterExtent(entries.length, entries));
      }));
   }
   
   @LegacySignature(returns = "OBJECT", type = Type.METHOD, name = "ResolveRelativeReference", qualified = "openedge.net.uri", parameters = {
            @LegacyParameter(name = "oUriBase", type = "OBJECT", qualified = "openedge.net.uri", mode = "INPUT"),
            @LegacyParameter(name = "cRelativeRef", type = "CHARACTER", mode = "INPUT"), })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static object<? extends Uri> resolveRelativeReference(object<? extends Uri> oUriBase,
            character cRelativeRef)
   {
      object<Uri> obj = TypeFactory.object(Uri.class);

      return function(Uri.class, "ResolveRelativeReference", object.class,
               new Block((Body) () -> {
                  Assert.notNull(oUriBase, new character("Base URI"));

                  String relRef = cRelativeRef.getValue();
                  character[] base = oUriBase.ref().split();
                  
                  // nothing to do, return base object
                  if (relRef == null || relRef.trim().isEmpty())
                  {
                     // same schema, authority, path and query but fragment is reset
                     obj.assign(ObjectOps.newInstance(Uri.class, "IIIII",
                              base[_schemePartIdx], base[_authorityPartIdx],
                              base[_pathPartIdx], base[_queryPartIdx], ""));
                  }
                  else
                  {

                     String[] relative = _splitUri(relRef);

                     // no scheme, everything is considered path
                     if ("".equals(relative[0]))
                     {
                        // if new path is absolute
                        if (relative[_pathPartIdx].startsWith("/")) {
                           obj.assign(ObjectOps.newInstance(Uri.class, "IIIII",
                                    base[_schemePartIdx], base[_authorityPartIdx],
                                    relative[_pathPartIdx], relative[_queryPartIdx],
                                    relative[_fragmentPartIdx]));
                        }
                        else {
                           // relative path, resolve it against current
                           obj.assign(ObjectOps.newInstance(Uri.class, "IIIII",
                                    base[_schemePartIdx], base[_authorityPartIdx],
                                    _resolvePath(base[_pathPartIdx].getValue(), relative[_pathPartIdx]), 
                                    relative[_queryPartIdx], relative[_fragmentPartIdx]));
                        }
                     }
                     else
                     {
                        // different scheme, complete override
                        obj.assign(ObjectOps.newInstance(Uri.class, "IIIII",
                                 relative[_schemePartIdx], relative[_authorityPartIdx],
                                 relative[_pathPartIdx], relative[_queryPartIdx],
                                 relative[_fragmentPartIdx]));
                     }

                  }

                  returnNormal(obj);
               }));
   }
   
   /**
    * Parses an potentially-URI-containing string into URI component parts.
    * This method will not fail, and will return empty values for an element of the
    * URI if the string cannot be parsed or fails to parse somehow. So if the string contains
    * http//example.com
    * the auhority will be 'example.com' and the scheme empty (since there's no :).
    * [1] scheme (http/https)
    * [2] authority (host:port)
    * [3] path
    * [4] query
    * [5] fragment
    * 
    * @param pData the URI string
    * 
    * @return An array of component parts
    */
   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "SplitUri", extent = -1, parameters = {
            @LegacyParameter(name = "pData", type = "CHARACTER", mode = "INPUT") })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_STUB)
   public static character[] splitUri(final character pData)
   {
      character[] res = TypeFactory.characterExtent();
      
      return extentFunction(Uri.class, "SplitUri", character.class, new Block((Body) () -> {
         String[] entries = _splitUri(pData.getValue());
         if (entries.length == 0)
         {
            returnExtentNormal(res);
         }
         
         returnExtentNormal(TypeFactory.characterExtent(entries.length, entries));
      }));
   }
   
   private static String[] _splitUri(final String pData) {
      
      String[] entries = new String[] {"", "", "", "", ""};
      
      if (pData != null && !pData.trim().isEmpty()) {
         // first position seems to be used, doesn't even have to be after scheme/authority
         int endIdx = pData.indexOf("#");
         
         // fragment seems to be extracted first, everything after first # is considered fragment
         if (endIdx != -1) {
            entries[_fragmentPartIdx] = _substring(pData, endIdx + 1, -1);
         }
         
         // query comes second, everything between first ? and fragment separator or end of string
         int qryIdx = pData.indexOf("?");
         
         if (qryIdx != -1 && (endIdx == -1 || qryIdx < endIdx)) {
            entries[_queryPartIdx] = _substring(pData, qryIdx + 1, endIdx);
            endIdx = qryIdx;
         }

         // if no scheme/authority everything lands in path
         int pathIdx = 0;
         
         // scheme separator must be before query/fragment else is ignored
         int schemaIdx = pData.indexOf(":");
         
         if (schemaIdx > 0 && (endIdx == -1 || schemaIdx < endIdx)) 
         {
            // remaining // separator is not required for schema extraction
            entries[_schemePartIdx] = _substring(pData, 0, schemaIdx++);
            pathIdx = schemaIdx;
         }
         
         int authIdx = pData.indexOf("//", schemaIdx);
         
         if (authIdx != -1 && (endIdx == -1 || authIdx < endIdx)) {
            // if we have authority everything up to path/query/fragment is used
            authIdx += 2;
            pathIdx = pData.indexOf("/", authIdx);
            
            entries[_authorityPartIdx] = _substring(pData, authIdx, pathIdx != -1 ? pathIdx : endIdx); 
         }
         
         // everything up to query/fragment seems to land in path
         if(pathIdx != -1)
            entries[_pathPartIdx] = _substring(pData, pathIdx, endIdx);
         
      }
      
      return entries;
   }
   
   // From URLDecoder, 4GL does not throw error on incomplete or invalid escape sequence (%) 
   // nor does replace plus with space like Java decoder does
   private static String _decode(String s) throws UnsupportedEncodingException
   {

      boolean needToChange = false;
      int numChars = s.length();
      StringBuffer sb = new StringBuffer(numChars > 500 ? numChars / 2 : numChars);
      int i = 0;

      char c;
      while (i < numChars)
      {
         c = s.charAt(i);
         byte[] bytes = null;
         switch (c)
         {
            case '%':
               /*
                * Starting with this instance of %, process all
                * consecutive substrings of the form %xy. Each
                * substring %xy will yield a byte. Convert all
                * consecutive  bytes obtained this way to whatever
                * character(s) they represent in the provided
                * encoding.
                */
               if (bytes == null)
                  bytes = new byte[4];
               int pos = 0;
               int expected = -1;

               while (((i + 2) < numChars) && (c == '%'))
               {
                  try
                  {
                     int v = Integer.parseInt(s.substring(i + 1, i + 3), 16);
                     // not an error in 4gl, left not decoded
                     if (v < 0)
                        break;

                     if (pos == 0)
                     {
                        if (v <= 128)
                        {
                           expected = 1;
                        }
                        // for UTF-8 only sequence start seems to be validated
                        // - C0-C1, F5-FF invalid
                        // - C2-DF (2 bytes)
                        // - E0-EF (3 bytes)
                        // - F0-F4 (4 bytes)
                        else if (v >= 194 && v < 245)
                        {
                           if (v < 224)
                              expected = 2;
                           else if (v < 240)
                              expected = 3;
                           else
                              expected = 4;
                        }
                        else
                        {
                           expected = -1;
                        }
                     }

                     bytes[pos++] = (byte) v;
                     i += 3;
                     if (i < numChars)
                        c = s.charAt(i);

                     // already a full sequence, reset position
                     if (c == '%' && pos == expected)
                     {
                        sb.append(new String(bytes, 0, pos, StandardCharsets.UTF_8));
                        needToChange = true;
                        pos = 0;
                     }
                  }
                  catch (NumberFormatException nfe)
                  {
                     pos = -1;
                     break;
                  }
               }

               // at least one escape sequence found
               if (pos >= 0)
               {
                  // last escape sequence in the list, not yet decoded
                  if (pos > 0)
                  {
                     if (expected != pos)
                        throw new UnsupportedEncodingException();

                     sb.append(new String(bytes, 0, pos, StandardCharsets.UTF_8));
                     needToChange = true;
                  }
                  break;
               }

            default:
               sb.append(c);
               i++;
               break;
         }
      }

      return (needToChange ? sb.toString() : s);
   }
   
   private static String _substring(String data, int start, int end) {
      if (data == null || start < 0)
         return data;
      
      if (end == -1)
         return data.substring(start);
      
      return data.substring(start, end);
   }
   
   private static String _resolvePath(String base, String relative) {
      if (!base.endsWith("/"))
         base += "/";
      
      String[] entries = TextOps.entries(base + "../" + relative, "/");
      int crt = 0;
      
      for (int i = 0; i < entries.length; i++)
      {
         if (".".equals(entries[i])) {
            // current, ignored
         } else if ("..".equals(entries[i])) {
            // safeguard, can't pass up the root
            if (crt > 0)
               crt--;
         } else {
            entries[crt++] = entries[i];
         }
      }
      
      return Arrays.stream(entries, 0, crt).reduce("/", (s, e) -> {
         if ("/".equals(s))
            return s + e;
         
         return s + "/" + e;
      });
   }
   
}