Skip to content

Patch ERFoundation to include src/main/woresources for Model resource… - #1039

Merged
hugithordarson merged 1 commit into
wocommunity:masterfrom
hugithordarson:patch_nsmavenprojectbundle_model_resources
May 29, 2026
Merged

Patch ERFoundation to include src/main/woresources for Model resource…#1039
hugithordarson merged 1 commit into
wocommunity:masterfrom
hugithordarson:patch_nsmavenprojectbundle_model_resources

Conversation

@hugithordarson

@hugithordarson hugithordarson commented Apr 15, 2026

Copy link
Copy Markdown
Member

Patches NSMavenProjectBundle.relativePathForResourceType via ASM so the NSResourceType.Model branch also checks for src/main/woresources (mirroring the existing D2WModel branch).

Bumps ERFoundation to 1.2, which is already deployed, containing the change.

The full source for the ASM patch (as written by Claude Code, since I won't touch ASM, but I've verified the results. Only NSMavenProjectBundle.class is modified, and decompiles to the proper looking class. Everything else is the same).

import java.nio.file.Files;
import java.nio.file.Path;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 * Patches NSMavenProjectBundle.relativePathForResourceType so that the
 * NSResourceType.Model branch also adds "src/main/woresources" (in addition
 * to the existing "src/main/resources").
 *
 * The technique is to detect the exact instruction pattern produced by the
 * original Model branch:
 *
 *     getstatic NSResourceType.Model
 *     aload_1
 *     if_acmpne <else>
 *     aload_2
 *     ldc "src/main/resources"
 *     invokeinterface List.add
 *     pop
 *     goto <end>
 *
 * and insert an additional "aload_2 / ldc "src/main/woresources" / invokeinterface List.add / pop"
 * before the terminating goto. All other methods and branches pass through ClassVisitor
 * unchanged. Class is Java 5 (v49), so there are no StackMapTable frames to worry about.
 */
public class PatchNSMavenProjectBundleAsm {

	private static final String TARGET_CLASS = "com/webobjects/foundation/development/NSMavenProjectBundle";
	private static final String RESOURCE_TYPE = "com/webobjects/foundation/development/NSResourceType";
	private static final String LIST_ITF = "java/util/List";
	private static final String ADD_DESC = "(Ljava/lang/Object;)Z";
	private static final String EXISTING_STRING = "src/main/resources";
	private static final String NEW_STRING = "src/main/woresources";

	public static void main( String[] args ) throws Exception {
		Path path = Path.of( args[0] );
		byte[] bytes = Files.readAllBytes( path );

		ClassReader cr = new ClassReader( bytes );
		ClassWriter cw = new ClassWriter( 0 );
		ClassVisitor cv = new ClassVisitor( Opcodes.ASM9, cw ) {
			@Override
			public MethodVisitor visitMethod( int acc, String name, String desc, String sig, String[] ex ) {
				MethodVisitor mv = super.visitMethod( acc, name, desc, sig, ex );
				if( !"relativePathForResourceType".equals( name ) ) {
					return mv;
				}
				return new ModelBranchPatcher( mv );
			}
		};
		cr.accept( cv, 0 );

		Path out = path.resolveSibling( path.getFileName() + ".patched" );
		Files.write( out, cw.toByteArray() );
		System.out.println( "Wrote: " + out );
	}

	/**
	 * State machine that detects the Model branch as a linear sequence of visit calls
	 * and injects four extra instructions just before its terminating GOTO.
	 */
	private static class ModelBranchPatcher extends MethodVisitor {

		/** Steps of the expected pattern. */
		private enum S {
			IDLE,
			SEEN_GETSTATIC_MODEL,
			SEEN_ALOAD_1,
			SEEN_IF_ACMPNE,
			SEEN_ALOAD_2,
			SEEN_LDC_RESOURCES,
			SEEN_INVOKE_ADD,
			SEEN_POP
		}

		private S state = S.IDLE;
		private int patchCount = 0;

		ModelBranchPatcher( MethodVisitor mv ) {
			super( Opcodes.ASM9, mv );
		}

		private void reset() {
			state = S.IDLE;
		}

		@Override
		public void visitFieldInsn( int opcode, String owner, String name, String descriptor ) {
			super.visitFieldInsn( opcode, owner, name, descriptor );
			if( opcode == Opcodes.GETSTATIC && RESOURCE_TYPE.equals( owner ) && "Model".equals( name ) ) {
				state = S.SEEN_GETSTATIC_MODEL;
			}
			else {
				reset();
			}
		}

		@Override
		public void visitVarInsn( int opcode, int var ) {
			super.visitVarInsn( opcode, var );
			if( state == S.SEEN_GETSTATIC_MODEL && opcode == Opcodes.ALOAD && var == 1 ) {
				state = S.SEEN_ALOAD_1;
			}
			else if( state == S.SEEN_IF_ACMPNE && opcode == Opcodes.ALOAD && var == 2 ) {
				state = S.SEEN_ALOAD_2;
			}
			else {
				reset();
			}
		}

		@Override
		public void visitJumpInsn( int opcode, Label label ) {
			if( state == S.SEEN_POP && opcode == Opcodes.GOTO ) {
				// Inject: aload_2; ldc "src/main/woresources"; invokeinterface List.add; pop
				super.visitVarInsn( Opcodes.ALOAD, 2 );
				super.visitLdcInsn( NEW_STRING );
				super.visitMethodInsn( Opcodes.INVOKEINTERFACE, LIST_ITF, "add", ADD_DESC, true );
				super.visitInsn( Opcodes.POP );
				patchCount++;
				super.visitJumpInsn( opcode, label );
				reset();
				return;
			}
			super.visitJumpInsn( opcode, label );
			if( state == S.SEEN_ALOAD_1 && opcode == Opcodes.IF_ACMPNE ) {
				state = S.SEEN_IF_ACMPNE;
			}
			else {
				reset();
			}
		}

		@Override
		public void visitLdcInsn( Object value ) {
			super.visitLdcInsn( value );
			if( state == S.SEEN_ALOAD_2 && EXISTING_STRING.equals( value ) ) {
				state = S.SEEN_LDC_RESOURCES;
			}
			else {
				reset();
			}
		}

		@Override
		public void visitMethodInsn( int opcode, String owner, String name, String descriptor, boolean itf ) {
			super.visitMethodInsn( opcode, owner, name, descriptor, itf );
			if( state == S.SEEN_LDC_RESOURCES
					&& opcode == Opcodes.INVOKEINTERFACE
					&& LIST_ITF.equals( owner )
					&& "add".equals( name )
					&& ADD_DESC.equals( descriptor ) ) {
				state = S.SEEN_INVOKE_ADD;
			}
			else {
				reset();
			}
		}

		@Override
		public void visitInsn( int opcode ) {
			super.visitInsn( opcode );
			if( state == S.SEEN_INVOKE_ADD && opcode == Opcodes.POP ) {
				state = S.SEEN_POP;
			}
			else {
				reset();
			}
		}

		@Override
		public void visitEnd() {
			super.visitEnd();
			if( patchCount != 1 ) {
				throw new IllegalStateException( "Expected exactly one Model branch patch, got " + patchCount );
			}
		}
	}
}

Closes #1034

… type

Patches NSMavenProjectBundle.relativePathForResourceType via ASM so the
NSResourceType.Model branch also adds "src/main/woresources", mirroring
the existing D2WModel branch. Bumps ERFoundation to 1.2.
@nullterminated

Copy link
Copy Markdown
Member

I should give you a tour of the new NSBundle sometime. It's way better. I still have a few things I want to improve about it, but the ERFoundation jar is going away in Wonder8... soon.

@hugithordarson

Copy link
Copy Markdown
Member Author

I'd love that. In the meantime, see anything wrong with merging this one?

@nullterminated

Copy link
Copy Markdown
Member

Nope, seems fine. Anyone can override the version back down in their own pom if they have an issue with it.

@hugithordarson
hugithordarson merged commit d47526f into wocommunity:master May 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix ERFoundation to look for EOModels in the woresources folder

2 participants